#include "Knob.h" namespace serum { namespace { // One shared look-and-feel for every knob. It outlives every Knob because // it is a function-local static and the GUI owns the knobs. KnobLookAndFeel& knobStyle() { static KnobLookAndFeel style; return style; } } Knob::Knob (const juce::String& name, std::function fmt) : formatter (std::move (fmt)) { setSliderStyle (juce::Slider::RotaryVerticalDrag); setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); setRange (0.0, 1.0); setDoubleClickReturnValue (true, 0.5); setName (name); setComponentID (name); // Draw the dial and the name strip through the theme, not through the // shared default look-and-feel. setLookAndFeel (&knobStyle()); } juce::String Knob::getTextFromValue (double value) { if (formatter) return formatter ((float) value); return formatPercent ((float) value); } // The dial takes the top of the footprint and the name takes a strip at the // bottom, so the name is always visible without a tooltip hover. void Knob::resized() { juce::Slider::resized(); textArea = getLocalBounds().removeFromBottom ( juce::jmin (KnobLookAndFeel::labelHeight, getHeight() / 2)); } void Knob::paint (juce::Graphics& g) { juce::Slider::paint (g); if (textArea.isEmpty()) return; // Dim while disabled, bright while hovered, otherwise the normal label // colour, so the three states are distinguishable without colour alone. const auto colour = isEnabled() ? (isMouseOverOrDragging() ? theme::text : theme::textDim) : theme::outline; g.setColour (colour); g.setFont (juce::Font (11.0f)); g.drawText (getName(), textArea, juce::Justification::centred, false); } void Knob::mouseDown (const juce::MouseEvent& e) { juce::Slider::mouseDown (e); showValuePopup (e); } void Knob::mouseDrag (const juce::MouseEvent& e) { juce::Slider::mouseDrag (e); showValuePopup (e); } void Knob::mouseUp (const juce::MouseEvent& e) { juce::Slider::mouseUp (e); hideValuePopup(); } void Knob::mouseExit (const juce::MouseEvent& e) { juce::Slider::mouseExit (e); hideValuePopup(); } // --------------------------------------------------------------------------- // The floating readout lives as a child of the top-level component so it can // follow the cursor outside this knob's own bounds without being clipped, and // it never intercepts mouse clicks so it can't block interaction. // --------------------------------------------------------------------------- void Knob::showValuePopup (const juce::MouseEvent& e) { auto* topLevel = getTopLevelComponent(); if (topLevel == nullptr) return; constexpr int baseW = 84; constexpr int baseH = 24; constexpr int gap = 10; // The interface scale is applied to a parent component, not to this slider. // Measure it from the local-space distance between two neighbouring points, // so a scaled interface does not leave the readout at a fixed size. float scale = 1.0f; if (auto* parent = getParentComponent()) { const auto origin = getLocalPoint (parent, juce::Point (0, 0)).toFloat(); const auto unitX = getLocalPoint (parent, juce::Point (1, 0)).toFloat(); scale = origin.getDistanceFrom (unitX); } scale = juce::jlimit (0.5f, 2.5f, scale); const int w = juce::roundToInt (baseW * scale); const int h = juce::roundToInt (baseH * scale); if (valuePopup == nullptr) { valuePopup = std::make_unique(); valuePopup->setAlwaysOnTop (true); valuePopup->setInterceptsMouseClicks (false, false); valuePopup->setColour (juce::Label::backgroundColourId, juce::Colours::transparentBlack); valuePopup->setColour (juce::Label::textColourId, theme::text); valuePopup->setJustificationType (juce::Justification::centred); topLevel->addAndMakeVisible (valuePopup.get()); } valuePopup->setFont (juce::Font (14.0f * scale, juce::Font::bold)); valuePopup->setText (getTextFromValue (getValue()), juce::dontSendNotification); // Keep the readout inside the window so a knob near an edge still shows it. const auto popupPos = topLevel->getLocalPoint (nullptr, e.getScreenPosition()); valuePopup->setBounds (juce::jlimit (0, juce::jmax (0, topLevel->getWidth() - w), popupPos.getX() - w / 2), juce::jlimit (0, juce::jmax (0, topLevel->getHeight() - h), popupPos.getY() - h - gap), w, h); } void Knob::hideValuePopup() { valuePopup.reset(); } // A section switch can hide the view holding this knob mid-drag. The popup is a // sibling of that view, so it has to be taken down here or it stays on screen. void Knob::visibilityChanged() { if (! isVisible()) hideValuePopup(); } } // namespace serum