@@ -13,14 +13,19 @@ struct Knob : ParamWidget { | |||||
struct Internal; | struct Internal; | ||||
Internal* internal; | Internal* internal; | ||||
/** Multiplier for mouse movement to adjust knob value */ | |||||
float speed = 1.0; | |||||
/** Drag horizontally instead of vertically. */ | /** Drag horizontally instead of vertically. */ | ||||
bool horizontal = false; | bool horizontal = false; | ||||
/** Enables per-sample value smoothing while dragging. */ | /** Enables per-sample value smoothing while dragging. */ | ||||
bool smooth = true; | bool smooth = true; | ||||
/** DEPRECATED. Use `ParamQuantity::snapEnabled`. */ | /** DEPRECATED. Use `ParamQuantity::snapEnabled`. */ | ||||
bool snap = false; | bool snap = false; | ||||
/** Multiplier for mouse movement to adjust knob value */ | |||||
float speed = 1.f; | |||||
/** Force dragging to linear, e.g. for sliders. */ | |||||
bool forceLinear = false; | |||||
/** Angles in radians. */ | |||||
float minAngle = -M_PI; | |||||
float maxAngle = M_PI; | |||||
Knob(); | Knob(); | ||||
~Knob(); | ~Knob(); | ||||
@@ -30,6 +35,7 @@ struct Knob : ParamWidget { | |||||
void onDragStart(const event::DragStart& e) override; | void onDragStart(const event::DragStart& e) override; | ||||
void onDragEnd(const event::DragEnd& e) override; | void onDragEnd(const event::DragEnd& e) override; | ||||
void onDragMove(const event::DragMove& e) override; | void onDragMove(const event::DragMove& e) override; | ||||
void onDragLeave(const event::DragLeave& e) override; | |||||
}; | }; | ||||
@@ -8,13 +8,10 @@ namespace app { | |||||
struct SliderKnob : Knob { | struct SliderKnob : Knob { | ||||
// Bypass Knob's circular hitbox detection | |||||
void onHover(const event::Hover& e) override { | |||||
ParamWidget::onHover(e); | |||||
} | |||||
void onButton(const event::Button& e) override { | |||||
ParamWidget::onButton(e); | |||||
} | |||||
SliderKnob(); | |||||
void onHover(const event::Hover& e) override; | |||||
void onButton(const event::Button& e) override; | |||||
}; | }; | ||||
@@ -17,9 +17,6 @@ struct SvgKnob : Knob { | |||||
CircularShadow* shadow; | CircularShadow* shadow; | ||||
widget::TransformWidget* tw; | widget::TransformWidget* tw; | ||||
widget::SvgWidget* sw; | widget::SvgWidget* sw; | ||||
/** Angles in radians */ | |||||
float minAngle = 0.f; | |||||
float maxAngle = M_PI; | |||||
SvgKnob(); | SvgKnob(); | ||||
void setSvg(std::shared_ptr<Svg> svg); | void setSvg(std::shared_ptr<Svg> svg); | ||||
@@ -28,12 +28,12 @@ extern bool invertZoom; | |||||
extern float cableOpacity; | extern float cableOpacity; | ||||
extern float cableTension; | extern float cableTension; | ||||
enum KnobMode { | enum KnobMode { | ||||
KNOB_MODE_LINEAR_LOCKED = 0, | |||||
KNOB_MODE_LINEAR, | KNOB_MODE_LINEAR, | ||||
KNOB_MODE_LINEAR_UNLOCKED, | |||||
KNOB_MODE_LINEAR_SPEED, | |||||
KNOB_MODE_LINEAR_SPEED_UNLOCKED, | |||||
KNOB_MODE_CIRCULAR_ABSOLUTE, | |||||
KNOB_MODE_CIRCULAR_RELATIVE, | |||||
KNOB_MODE_SCALED_LINEAR_LOCKED = 100, | |||||
KNOB_MODE_SCALED_LINEAR, | |||||
KNOB_MODE_ROTARY_ABSOLUTE = 200, | |||||
KNOB_MODE_ROTARY_RELATIVE, | |||||
}; | }; | ||||
extern KnobMode knobMode; | extern KnobMode knobMode; | ||||
extern float sampleRate; | extern float sampleRate; | ||||
@@ -3,20 +3,26 @@ | |||||
#include <app/Scene.hpp> | #include <app/Scene.hpp> | ||||
#include <random.hpp> | #include <random.hpp> | ||||
#include <history.hpp> | #include <history.hpp> | ||||
#include <settings.hpp> | |||||
namespace rack { | namespace rack { | ||||
namespace app { | namespace app { | ||||
static const float KNOB_SENSITIVITY = 0.0015f; | |||||
struct Knob::Internal { | struct Knob::Internal { | ||||
/** Value of the knob before dragging. */ | /** Value of the knob before dragging. */ | ||||
float oldValue = 0.f; | float oldValue = 0.f; | ||||
/** Fractional value between the param's value and the dragged knob position. */ | |||||
/** Fractional value between the param's value and the dragged knob position. | |||||
Using a "snapValue" variable and rounding is insufficient because the mouse needs to reach 1.0, not 0.5 to obtain the first increment. | |||||
*/ | |||||
float snapDelta = 0.f; | float snapDelta = 0.f; | ||||
/** Speed multiplier in speed knob mode */ | |||||
float linearScale = 1.f; | |||||
/** The mouse has once escaped from the knob while dragging. */ | |||||
bool rotaryDragEnabled = false; | |||||
float dragAngle = NAN; | |||||
}; | }; | ||||
@@ -38,6 +44,7 @@ void Knob::initParamQuantity() { | |||||
} | } | ||||
void Knob::onHover(const event::Hover& e) { | void Knob::onHover(const event::Hover& e) { | ||||
// Only call super if mouse position is in the circle | |||||
math::Vec c = box.size.div(2); | math::Vec c = box.size.div(2); | ||||
float dist = e.pos.minus(c).norm(); | float dist = e.pos.minus(c).norm(); | ||||
if (dist <= c.x) { | if (dist <= c.x) { | ||||
@@ -63,14 +70,27 @@ void Knob::onDragStart(const event::DragStart& e) { | |||||
internal->snapDelta = 0.f; | internal->snapDelta = 0.f; | ||||
} | } | ||||
APP->window->cursorLock(); | |||||
settings::KnobMode km = settings::knobMode; | |||||
if (km == settings::KNOB_MODE_LINEAR_LOCKED || km == settings::KNOB_MODE_SCALED_LINEAR_LOCKED) { | |||||
APP->window->cursorLock(); | |||||
} | |||||
// Only changed for KNOB_MODE_LINEAR_*. | |||||
internal->linearScale = 1.f; | |||||
// Only used for KNOB_MODE_ROTARY_*. | |||||
internal->rotaryDragEnabled = false; | |||||
internal->dragAngle = NAN; | |||||
ParamWidget::onDragStart(e); | |||||
} | } | ||||
void Knob::onDragEnd(const event::DragEnd& e) { | void Knob::onDragEnd(const event::DragEnd& e) { | ||||
if (e.button != GLFW_MOUSE_BUTTON_LEFT) | if (e.button != GLFW_MOUSE_BUTTON_LEFT) | ||||
return; | return; | ||||
APP->window->cursorUnlock(); | |||||
settings::KnobMode km = settings::knobMode; | |||||
if (km == settings::KNOB_MODE_LINEAR_LOCKED || km == settings::KNOB_MODE_SCALED_LINEAR_LOCKED) { | |||||
APP->window->cursorUnlock(); | |||||
} | |||||
engine::ParamQuantity* pq = getParamQuantity(); | engine::ParamQuantity* pq = getParamQuantity(); | ||||
if (pq) { | if (pq) { | ||||
@@ -86,56 +106,123 @@ void Knob::onDragEnd(const event::DragEnd& e) { | |||||
APP->history->push(h); | APP->history->push(h); | ||||
} | } | ||||
} | } | ||||
ParamWidget::onDragEnd(e); | |||||
} | } | ||||
void Knob::onDragMove(const event::DragMove& e) { | void Knob::onDragMove(const event::DragMove& e) { | ||||
if (e.button != GLFW_MOUSE_BUTTON_LEFT) | if (e.button != GLFW_MOUSE_BUTTON_LEFT) | ||||
return; | return; | ||||
settings::KnobMode km = settings::knobMode; | |||||
bool linearMode = (km < settings::KNOB_MODE_ROTARY_ABSOLUTE) || forceLinear; | |||||
engine::ParamQuantity* pq = getParamQuantity(); | engine::ParamQuantity* pq = getParamQuantity(); | ||||
if (pq) { | if (pq) { | ||||
float range; | |||||
if (pq->isBounded()) { | |||||
range = pq->getRange(); | |||||
} | |||||
else { | |||||
// Continuous encoders scale as if their limits are +/-1 | |||||
range = 2.f; | |||||
} | |||||
float delta = (horizontal ? e.mouseDelta.x : -e.mouseDelta.y); | |||||
delta *= KNOB_SENSITIVITY; | |||||
delta *= speed; | |||||
delta *= range; | |||||
float value = smooth ? pq->getSmoothValue() : pq->getValue(); | |||||
// Drag slower if Mod is held | |||||
// Scale by mod keys | |||||
float modScale = 1.f; | |||||
// Drag slower if Ctrl is held | |||||
int mods = APP->window->getMods(); | int mods = APP->window->getMods(); | ||||
if ((mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | if ((mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | ||||
delta /= 16.f; | |||||
modScale /= 16.f; | |||||
} | } | ||||
// Drag even slower if Mod-Shift is held | |||||
// Drag even slower if Ctrl-Shift is held | |||||
if ((mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | if ((mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | ||||
delta /= 256.f; | |||||
modScale /= 256.f; | |||||
} | } | ||||
if (pq->snapEnabled) { | |||||
// Replace delta with an accumulated delta since the last integer knob. | |||||
internal->snapDelta += delta; | |||||
delta = std::trunc(internal->snapDelta); | |||||
internal->snapDelta -= delta; | |||||
// Ratio between parameter value scale / (angle range / 2*pi) | |||||
float rangeRatio; | |||||
if (pq->isBounded()) { | |||||
rangeRatio = pq->getRange(); | |||||
rangeRatio /= (maxAngle - minAngle) / float(2 * M_PI); | |||||
} | |||||
else { | |||||
rangeRatio = 1.f; | |||||
} | } | ||||
// Set value | |||||
if (smooth) { | |||||
pq->setSmoothValue(pq->getSmoothValue() + delta); | |||||
if (linearMode) { | |||||
float delta = (horizontal ? e.mouseDelta.x : -e.mouseDelta.y); | |||||
// TODO Put this in settings | |||||
const float linearSensitivity = 1 / 1000.f; | |||||
delta *= linearSensitivity; | |||||
delta *= modScale; | |||||
delta *= rangeRatio; | |||||
// Scale delta if in scaled linear knob mode | |||||
if (km == settings::KNOB_MODE_SCALED_LINEAR_LOCKED || km == settings::KNOB_MODE_SCALED_LINEAR) { | |||||
float deltaY = (horizontal ? -e.mouseDelta.y : -e.mouseDelta.x); | |||||
const float pixelTau = 200.f; | |||||
internal->linearScale *= std::pow(2.f, deltaY / pixelTau); | |||||
delta *= internal->linearScale; | |||||
} | |||||
// Handle value snapping | |||||
if (pq->snapEnabled) { | |||||
// Replace delta with an accumulated delta since the last integer knob. | |||||
internal->snapDelta += delta; | |||||
delta = std::trunc(internal->snapDelta); | |||||
internal->snapDelta -= delta; | |||||
} | |||||
value += delta; | |||||
} | } | ||||
else { | |||||
pq->setValue(pq->getValue() + delta); | |||||
else if (internal->rotaryDragEnabled) { | |||||
math::Vec origin = getAbsoluteOffset(box.size.div(2)); | |||||
math::Vec deltaPos = APP->scene->mousePos.minus(origin); | |||||
float angle = deltaPos.arg() + float(M_PI) / 2; | |||||
bool absoluteRotaryMode = (km == settings::KNOB_MODE_ROTARY_ABSOLUTE) && pq->isBounded(); | |||||
if (absoluteRotaryMode) { | |||||
// Find angle closest to midpoint of angle range, mod 2*pi | |||||
float midAngle = (minAngle + maxAngle) / 2; | |||||
angle = math::eucMod(angle - midAngle + float(M_PI), float(2 * M_PI)) + midAngle - float(M_PI); | |||||
value = math::rescale(angle, minAngle, maxAngle, pq->getMinValue(), pq->getMaxValue()); | |||||
} | |||||
else { | |||||
if (!std::isfinite(internal->dragAngle)) { | |||||
// Set the starting angle | |||||
internal->dragAngle = angle; | |||||
} | |||||
// Find angle closest to last angle, mod 2*pi | |||||
float deltaAngle = math::eucMod(angle - internal->dragAngle + float(M_PI), float(2 * M_PI)) - float(M_PI); | |||||
internal->dragAngle = angle; | |||||
float delta = deltaAngle / float(2 * M_PI) * rangeRatio; | |||||
delta *= modScale; | |||||
// Handle value snapping | |||||
if (pq->snapEnabled) { | |||||
// Replace delta with an accumulated delta since the last integer knob. | |||||
internal->snapDelta += delta; | |||||
delta = std::trunc(internal->snapDelta); | |||||
internal->snapDelta -= delta; | |||||
} | |||||
value += delta; | |||||
} | |||||
} | } | ||||
// Set value | |||||
if (smooth) | |||||
pq->setSmoothValue(value); | |||||
else | |||||
pq->setValue(value); | |||||
} | } | ||||
ParamWidget::onDragMove(e); | ParamWidget::onDragMove(e); | ||||
} | } | ||||
void Knob::onDragLeave(const event::DragLeave& e) { | |||||
if (e.origin == this) { | |||||
internal->rotaryDragEnabled = true; | |||||
} | |||||
ParamWidget::onDragLeave(e); | |||||
} | |||||
} // namespace app | } // namespace app | ||||
} // namespace rack | } // namespace rack |
@@ -19,6 +19,7 @@ | |||||
#include <updater.hpp> | #include <updater.hpp> | ||||
#include <osdialog.h> | #include <osdialog.h> | ||||
#include <thread> | #include <thread> | ||||
#include <utility> | |||||
namespace rack { | namespace rack { | ||||
@@ -327,19 +328,19 @@ struct KnobModeItem : ui::MenuItem { | |||||
ui::Menu* createChildMenu() override { | ui::Menu* createChildMenu() override { | ||||
ui::Menu* menu = new ui::Menu; | ui::Menu* menu = new ui::Menu; | ||||
static const std::string knobModeNames[] = { | |||||
"Linear (locked cursor)", | |||||
"Linear", | |||||
"Adjustable linear (locked cursor)", | |||||
"Adjustable linear", | |||||
"Absolute rotary", | |||||
"Relative rotary", | |||||
static const std::vector<std::pair<settings::KnobMode, std::string>> knobModes = { | |||||
{settings::KNOB_MODE_LINEAR_LOCKED, "Linear (locked cursor)"}, | |||||
{settings::KNOB_MODE_LINEAR, "Linear"}, | |||||
{settings::KNOB_MODE_SCALED_LINEAR_LOCKED, "Scaled linear (locked cursor)"}, | |||||
{settings::KNOB_MODE_SCALED_LINEAR, "Scaled linear"}, | |||||
{settings::KNOB_MODE_ROTARY_ABSOLUTE, "Absolute rotary"}, | |||||
{settings::KNOB_MODE_ROTARY_RELATIVE, "Relative rotary"}, | |||||
}; | }; | ||||
for (int i = 0; i < (int) LENGTHOF(knobModeNames); i++) { | |||||
for (const auto& pair : knobModes) { | |||||
KnobModeValueItem* item = new KnobModeValueItem; | KnobModeValueItem* item = new KnobModeValueItem; | ||||
item->knobMode = (settings::KnobMode) i; | |||||
item->text = knobModeNames[i]; | |||||
item->rightText = CHECKMARK(settings::knobMode == i); | |||||
item->knobMode = pair.first; | |||||
item->text = pair.second; | |||||
item->rightText = CHECKMARK(settings::knobMode == pair.first); | |||||
menu->addChild(item); | menu->addChild(item); | ||||
} | } | ||||
return menu; | return menu; | ||||
@@ -0,0 +1,23 @@ | |||||
#include <app/SliderKnob.hpp> | |||||
namespace rack { | |||||
namespace app { | |||||
SliderKnob::SliderKnob() { | |||||
forceLinear = true; | |||||
} | |||||
void SliderKnob::onHover(const event::Hover& e) { | |||||
// Bypass Knob's circular hitbox detection | |||||
ParamWidget::onHover(e); | |||||
} | |||||
void SliderKnob::onButton(const event::Button& e) { | |||||
ParamWidget::onButton(e); | |||||
} | |||||
} // namespace app | |||||
} // namespace rack |
@@ -38,14 +38,15 @@ void SvgKnob::onChange(const event::Change& e) { | |||||
float value = pq->getSmoothValue(); | float value = pq->getSmoothValue(); | ||||
float angle; | float angle; | ||||
if (!pq->isBounded()) { | if (!pq->isBounded()) { | ||||
// Center unbounded knobs | |||||
angle = math::rescale(value, -1.f, 1.f, minAngle, maxAngle); | |||||
// Number of rotations equals value for unbounded range | |||||
angle = value * (2 * M_PI); | |||||
} | } | ||||
else if (pq->getMinValue() == pq->getMaxValue()) { | |||||
// Center 0 range | |||||
angle = math::rescale(0.f, -1.f, 1.f, minAngle, maxAngle); | |||||
else if (pq->getRange() == 0.f) { | |||||
// Center angle for zero range | |||||
angle = (minAngle + maxAngle) / 2.f; | |||||
} | } | ||||
else { | else { | ||||
// Proportional angle for finite range | |||||
angle = math::rescale(value, pq->getMinValue(), pq->getMaxValue(), minAngle, maxAngle); | angle = math::rescale(value, pq->getMinValue(), pq->getMaxValue(), minAngle, maxAngle); | ||||
} | } | ||||
angle = std::fmod(angle, 2 * M_PI); | angle = std::fmod(angle, 2 * M_PI); | ||||
@@ -233,8 +233,9 @@ bool State::handleHover(math::Vec pos, math::Vec mouseDelta) { | |||||
bool State::handleLeave() { | bool State::handleLeave() { | ||||
heldKeys.clear(); | heldKeys.clear(); | ||||
setDragHovered(NULL); | |||||
setHovered(NULL); | |||||
// When leaving the window, don't un-hover widgets because the mouse might be dragging. | |||||
// setDragHovered(NULL); | |||||
// setHovered(NULL); | |||||
return true; | return true; | ||||
} | } | ||||
@@ -22,7 +22,7 @@ float zoom = 0.0; | |||||
bool invertZoom = false; | bool invertZoom = false; | ||||
float cableOpacity = 0.5; | float cableOpacity = 0.5; | ||||
float cableTension = 0.5; | float cableTension = 0.5; | ||||
KnobMode knobMode = KNOB_MODE_LINEAR; | |||||
KnobMode knobMode = KNOB_MODE_LINEAR_LOCKED; | |||||
float sampleRate = 44100.0; | float sampleRate = 44100.0; | ||||
int threadCount = 1; | int threadCount = 1; | ||||
bool paramTooltip = false; | bool paramTooltip = false; | ||||
@@ -145,7 +145,7 @@ void fromJson(json_t* rootJ) { | |||||
json_t* allowCursorLockJ = json_object_get(rootJ, "allowCursorLock"); | json_t* allowCursorLockJ = json_object_get(rootJ, "allowCursorLock"); | ||||
if (allowCursorLockJ) { | if (allowCursorLockJ) { | ||||
if (json_is_false(allowCursorLockJ)) | if (json_is_false(allowCursorLockJ)) | ||||
knobMode = KNOB_MODE_LINEAR_UNLOCKED; | |||||
knobMode = KNOB_MODE_LINEAR; | |||||
} | } | ||||
json_t* knobModeJ = json_object_get(rootJ, "knobMode"); | json_t* knobModeJ = json_object_get(rootJ, "knobMode"); | ||||