diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a260ae..6e87d71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ cmake_minimum_required(VERSION 3.22) # cmake --build build --config Release # ============================================================================ -project(Anamorph VERSION 0.6.0 LANGUAGES C CXX) +project(Anamorph VERSION 0.6.1 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/src/PluginEditor.cpp b/src/PluginEditor.cpp index 526cc7a..4eda176 100644 --- a/src/PluginEditor.cpp +++ b/src/PluginEditor.cpp @@ -233,6 +233,7 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess setLookAndFeel (&lnf); tooltips.setLookAndFeel (&lnf); + setOpaque (true); // fill our bounds every paint -> no see-through flash on a scale resize (#13) openGLContext.setContinuousRepainting (false); openGLContext.attachTo (*this); @@ -271,7 +272,7 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess presetName.setComponentID ("presetname"); presetPrev.setTooltip ("Previous preset"); presetNext.setTooltip ("Next preset"); - presetName.setTooltip ("Presets: click to browse, save and load."); + presetName.setTooltip ("Presets"); // short, no period (#12) presetPrev.onClick = [this] { processor.getPresets().step (-1); refreshPresetDisplay(); }; presetNext.onClick = [this] { processor.getPresets().step (+1); refreshPresetDisplay(); }; presetName.onClick = [this] { showPresetMenu(); }; @@ -293,6 +294,7 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess saveNameEditor.setColour (juce::TextEditor::outlineColourId, colours::outline); saveNameEditor.setColour (juce::TextEditor::focusedOutlineColourId, colours::accent.withAlpha (0.6f)); saveNameEditor.setColour (juce::TextEditor::highlightColourId, colours::accent.withAlpha (0.3f)); + saveNameEditor.getProperties().set ("glow", true); // accent micro-glow border (#11) saveNameEditor.setSelectAllWhenFocused (true); saveNameEditor.onReturnKey = [this] { saveOkButton.triggerClick(); }; saveNameEditor.onEscapeKey = [this] { showSavePreset (false); }; @@ -322,7 +324,7 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess setupToggle (metersToggle, pid::metersOn, "", "Show level meters."); // #32 metersToggle.setComponentID ("metersicon"); // level-meter glyph, not the word (#7) - metersToggle.onClick = [this] { metersOn = metersToggle.getToggleState(); if (metersOn) levelMeter->setVisible (true); }; + metersToggle.onClick = [this] { metersOn = metersToggle.getToggleState(); }; // visibility via layoutScopeArea (#2) setupToggle (advancedToggle, pid::advancedMode, "Adv", "Advanced mode"); // #17 advancedToggle.onClick = [this] { advanced = advancedToggle.getToggleState(); updateModeVisibility(); }; @@ -538,7 +540,7 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess buttonAtts.add (new ButtonAttachment (processor.getAPVTS(), pid::tooltipsOn, tooltipsToggle)); animToggle.setButtonText ("UI Animations"); - animToggle.setTooltip ("Smooth micro-animations on hovers, presses and switches (F3)."); + animToggle.setTooltip ("Smooth micro-animations on hovers, presses and switches."); // no F3 ref (#4) settingsBackdrop.addAndMakeVisible (animToggle); buttonAtts.add (new ButtonAttachment (processor.getAPVTS(), pid::uiAnimations, animToggle)); @@ -798,10 +800,7 @@ void AnamorphAudioProcessorEditor::timerCallback() updateModeVisibility(); } if (metersToggle.getToggleState() != metersOn) - { - metersOn = metersToggle.getToggleState(); - if (metersOn) levelMeter->setVisible (true); - } + metersOn = metersToggle.getToggleState(); // layoutScopeArea owns visibility (#2) if (tooltipsToggle.getToggleState() != tooltipsOn) { tooltipsOn = tooltipsToggle.getToggleState(); @@ -879,28 +878,27 @@ void AnamorphAudioProcessorEditor::timerCallback() matchReadout.setText (juce::String (processor.getEngine().getMatchGainDb(), 1) + " dB", juce::dontSendNotification); } -// Vsync-stepped meter reveal (#6): a fixed 240 ms ease-out sextic -- -// 1 - (1-t)^6 -- which launches faster than the old exponential and settles -// into a much gentler landing, without lengthening the overall move (#3). +// Vsync-stepped meter reveal: the v0.5.9 exponential ease (factor 0.55 per +// 1/24 s, time-corrected to whatever the display rate is) -- the prettier curve +// the user preferred (#1). Stateless, so it always converges and can never stall +// part-open (#2). Follows the UI-animation switch: off = snap instantly (#9). void AnamorphAudioProcessorEditor::stepMeterReveal (double dt) { const float target = metersOn ? 1.0f : 0.0f; - if (std::abs (meterAnim - target) < 1.0e-6f) return; // idle: one compare per frame - - if (std::abs (meterAnimTarget - target) > 0.5f) // (re)arm, also on mid-flight reversal + const float diff = std::abs (meterAnim - target); + if (diff < 1.0e-4f) { - meterAnimTarget = target; - meterAnimFrom = meterAnim; - meterAnimT = 0.0; + if (diff > 0.0f) { meterAnim = target; layoutScopeArea(); } // one final snap + return; // idle otherwise } - meterAnimT = juce::jmin (1.0, meterAnimT + dt / 0.24); - const float e = 1.0f - (float) std::pow (1.0 - meterAnimT, 6.0); - meterAnim = meterAnimFrom + (target - meterAnimFrom) * e; - if (meterAnimT >= 1.0) - { + + if (uiAnimOn) + meterAnim += (target - meterAnim) * (1.0f - (float) std::pow (0.45, dt * 24.0)); + else + meterAnim = target; + + if (std::abs (meterAnim - target) < 0.01f) meterAnim = target; - if (! metersOn) levelMeter->setVisible (false); - } layoutScopeArea(); } @@ -910,52 +908,74 @@ void AnamorphAudioProcessorEditor::registerAnimated (juce::Component& c) } // Micro-animation driver (F3): eases per-component "hovA" (hover), "actA" -// (press) and "onA" (toggle position) properties every display frame; the -// LookAndFeel blends its glows/lifts/knob travel with them. Fast in, gentler -// out -- the Apple-feel non-linearity -- and when the Settings switch is off -// the rates snap to 1 so everything behaves exactly as before. Idle cost is a -// handful of compares per control; repaints only fire while a value moves. +// (press), "onA" (toggle position) and "vpos" (knob value travel) properties +// every display frame; the LookAndFeel blends its glows/lifts/knob angle with +// them. Fast in, gentler out -- the Apple-feel non-linearity -- and when the +// Settings switch is off the rates snap to 1 so everything behaves exactly as +// before. Idle cost is a handful of compares per control; repaints only fire +// while a value is actually moving. void AnamorphAudioProcessorEditor::stepMicroAnims (double dt) { - static const juce::Identifier hovA ("hovA"), actA ("actA"), onA ("onA"); + static const juce::Identifier hovA ("hovA"), actA ("actA"), onA ("onA"), vpos ("vpos"); const float rIn = uiAnimOn ? 1.0f - std::exp (-(float) dt / 0.045f) : 1.0f; const float rOut = uiAnimOn ? 1.0f - std::exp (-(float) dt / 0.120f) : 1.0f; const float rAct = uiAnimOn ? 1.0f - std::exp (-(float) dt / 0.025f) : 1.0f; const float rOn = uiAnimOn ? 1.0f - std::exp (-(float) dt / 0.055f) : 1.0f; + const float rPos = uiAnimOn ? 1.0f - std::exp (-(float) dt / 0.090f) : 1.0f; for (auto* c : animated) { - float hovT = 0.0f, actT = -1.0f, onT = -1.0f; + auto& props = c->getProperties(); - if (auto* s = dynamic_cast (c)) - { - hovT = s->isMouseOver (false) ? 1.0f : 0.0f; - actT = (s->isMouseButtonDown() - || (bool) s->getProperties().getWithDefault ("dragging", false)) ? 1.0f : 0.0f; - } - else if (auto* b = dynamic_cast (c)) - { - hovT = b->isOver() ? 1.0f : 0.0f; - if (auto* t = dynamic_cast (c)) - onT = t->getToggleState() ? 1.0f : 0.0f; - } - else if (auto* box = dynamic_cast (c)) - hovT = (bool) box->getProperties().getWithDefault ("hov", false) ? 1.0f : 0.0f; - else - hovT = c->isMouseOver (true) ? 1.0f : 0.0f; // A/B control + // Hover is hit-tested against the live cursor rather than read from + // enter/exit events: those fire unreliably across clicks, relayouts and + // pop-ups, which is what made a click occasionally flicker (#10). + const bool over = c->isShowing() + && c->getLocalBounds().contains (c->getMouseXYRelative()); + float hovT = over ? 1.0f : 0.0f; + float actT = -1.0f, onT = -1.0f; - auto& props = c->getProperties(); auto stepVal = [&props] (const juce::Identifier& key, float target, float up, float down) -> bool { - const float curr = (float) (double) props.getWithDefault (key, 0.0); + const float curr = (float) (double) props.getWithDefault (key, (double) target); float next = curr + (target - curr) * (target > curr ? up : down); if (std::abs (next - target) < 0.004f) next = target; if (std::abs (next - curr) < 0.0015f) return false; - props.set (key, next); + props.set (key, juce::jlimit (0.0f, 1.0f, next)); return true; }; + if (auto* s = dynamic_cast (c)) + { + const bool interacting = s->isMouseButtonDown() + || (bool) props.getWithDefault ("dragging", false); + actT = interacting ? 1.0f : 0.0f; + + // Rotary knobs ease their pointer/arc toward the live value, so a + // preset or A/B switch SWEEPS the knob instead of teleporting it (#5). + // While the user turns it, snap so the knob stays 1:1 (no lag). vpos is + // ALWAYS kept current (even at rest) so there's a real "from" position + // to ease out of the instant the value jumps. + const auto st = s->getSliderStyle(); + const bool rotary = st == juce::Slider::RotaryVerticalDrag + || st == juce::Slider::RotaryHorizontalDrag + || st == juce::Slider::RotaryHorizontalVerticalDrag + || st == juce::Slider::Rotary; + if (rotary) + { + const float realPos = (float) s->valueToProportionOfLength (s->getValue()); + const float curr = (float) (double) props.getWithDefault (vpos, (double) realPos); + float vp = (interacting || ! uiAnimOn) ? realPos + : curr + (realPos - curr) * rPos; + if (std::abs (vp - realPos) < 0.0015f) vp = realPos; + if (std::abs (vp - curr) > 0.0004f) c->repaint(); + props.set (vpos, vp); + } + } + else if (auto* t = dynamic_cast (c)) + onT = t->getToggleState() ? 1.0f : 0.0f; + bool changed = stepVal (hovA, hovT, rIn, rOut); if (actT >= 0.0f) changed = stepVal (actA, actT, rAct, rOut) || changed; if (onT >= 0.0f) changed = stepVal (onA, onT, rOn, rOn) || changed; @@ -971,18 +991,70 @@ void AnamorphAudioProcessorEditor::applyUiScale() { static constexpr float scales[] = { 0.75f, 0.85f, 1.0f, 1.25f, 1.5f }; const int idx = juce::jlimit (0, 4, uiScaleBox.getSelectedItemIndex()); - lastScaleIdx = uiScaleBox.getSelectedItemIndex(); + if (idx == lastScaleIdx) return; // no redundant resizes (#13) + lastScaleIdx = idx; + + // Apply width AND height in a single transform change so the wrapper issues + // one resize, not a width-then-height pair that flashes an L-shape (#13). The + // editor is opaque and repaints synchronously at the new size, so no stale or + // see-through frame is shown mid-resize. setTransform (juce::AffineTransform::scale (scales[idx])); + if (openGLContext.isAttached()) + openGLContext.triggerRepaint(); } // ---------------------------------------------------------------------------- // Preset browser (F2) // ---------------------------------------------------------------------------- +namespace +{ + // Pixel width of a string in a font, via GlyphArrangement (no deprecated API). + static float textWidth (const juce::Font& f, const juce::String& s) + { + if (s.isEmpty()) return 0.0f; + juce::GlyphArrangement ga; + ga.addLineOfText (f, s, 0.0f, 0.0f); + return ga.getBoundingBox (0, -1, true).getWidth(); + } + + // Pro Tools-style track-name abbreviation: keep each word's first letter, drop + // the vowels from the rest ("Drum" -> "Drm", "Stereo" -> "Stro"), so a long + // preset name still reads when the slot is narrow (#7). + static juce::String abbreviate (const juce::String& name) + { + auto words = juce::StringArray::fromTokens (name, " ", ""); + juce::String out; + for (const auto& word : words) + { + if (word.isEmpty()) continue; + if (out.isNotEmpty()) out << ' '; + out << word[0]; + for (int i = 1; i < word.length(); ++i) + if (! juce::String ("aeiouAEIOU").containsChar (word[i])) + out << word[i]; + } + return out; + } +} + void AnamorphAudioProcessorEditor::refreshPresetDisplay() { auto& pm = processor.getPresets(); - const juce::String shown = pm.currentName() - + (pm.isDirty() ? juce::String (" ") + juce::String::charToString ((juce::juce_wchar) 0x2022) : juce::String()); + // A small asterisk marks an edited preset -- lighter than the old bullet (#6). + const juce::String marker = pm.isDirty() ? " *" : juce::String(); + + const juce::Font font (juce::FontOptions (13.0f)); // matches the presetname button font + const float avail = (float) presetName.getWidth() - 12.0f - textWidth (font, marker); + + juce::String name = pm.currentName(); + if (textWidth (font, name) > avail) + { + name = abbreviate (pm.currentName()); // consonant skeleton (#7) + while (name.isNotEmpty() && textWidth (font, name) > avail) + name = name.dropLastCharacters (1); // hard-clip if still too wide + } + + const juce::String shown = name + marker; if (presetName.getButtonText() != shown) presetName.setButtonText (shown); } @@ -1003,21 +1075,42 @@ void AnamorphAudioProcessorEditor::showPresetMenu() if (! e.isFactory && ! userHeader) { m.addSectionHeader ("USER"); userHeader = true; } m.addItem (i + 1, e.name, true, i == cur); } + const juce::String ellip = juce::String::charToString ((juce::juce_wchar) 0x2026); m.addSeparator(); - m.addItem (10001, juce::String ("Save Preset") + juce::String::charToString ((juce::juce_wchar) 0x2026)); + m.addItem (10001, "Save Preset" + ellip); + m.addItem (10002, "Load Preset" + ellip); // OS file chooser (#3) + // Widen the list so the longest factory name ("Synth Dimension") shows in + // full -- the slot itself is narrow (#8). m.showMenuAsync (juce::PopupMenu::Options() .withTargetComponent (presetName) - .withMinimumWidth (presetName.getWidth()), + .withMinimumWidth (228), [this] (int r) { if (r == 0) return; if (r == 10001) { showSavePreset (true); return; } + if (r == 10002) { showLoadPreset(); return; } processor.getPresets().load (r - 1); refreshPresetDisplay(); }); } +void AnamorphAudioProcessorEditor::showLoadPreset() +{ + auto dir = anamorph::PresetManager::presetDirectory(); + dir.createDirectory(); // so the chooser opens somewhere sensible even when empty + fileChooser = std::make_unique ( + "Load Anamorph Preset", dir, "*" + anamorph::PresetManager::fileSuffix()); + + fileChooser->launchAsync (juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles, + [this] (const juce::FileChooser& fc) + { + const auto file = fc.getResult(); + if (file.existsAsFile() && processor.getPresets().loadFile (file)) + refreshPresetDisplay(); + }); +} + void AnamorphAudioProcessorEditor::showSavePreset (bool show) { savePresetBackdrop.setVisible (show); @@ -1087,7 +1180,13 @@ void AnamorphAudioProcessorEditor::layoutScopeArea() // scope's (and the correlation meter's) top/bottom edges (#1). const int meterFull = 154; // fits 8 numbers + dB ruler; bars stay thin (#11/#17) const int reserve = juce::roundToInt (meterFull * meterAnim); - if (reserve > 2) + const bool showMeter = reserve > 2; + // Visibility is decided HERE, every animation frame: the moment the reserve + // collapses past a couple of px the meter is hidden, so it can never be left + // as a 1-2 px sliver on the left (the stuck strip, #2). + if (levelMeter->isVisible() != showMeter) + levelMeter->setVisible (showMeter); + if (showMeter) { levelMeter->setBounds (sa.removeFromLeft (reserve)); sa.removeFromLeft (juce::roundToInt (12.0f * meterAnim)); diff --git a/src/PluginEditor.h b/src/PluginEditor.h index 2a3d535..e4f47cb 100644 --- a/src/PluginEditor.h +++ b/src/PluginEditor.h @@ -72,9 +72,10 @@ class AnamorphAudioProcessorEditor : public juce::AudioProcessorEditor, void registerAnimated (juce::Component&); void mouseWheelMove (const juce::MouseEvent&, const juce::MouseWheelDetails&) override; // Persist scroll reveal (#1) void applyUiScale(); // whole-window XS..XL transform scale (F4) - void refreshPresetDisplay(); // preset name + dirty dot (F2) + void refreshPresetDisplay(); // preset name + dirty mark (F2) void showPresetMenu(); void showSavePreset (bool); + void showLoadPreset(); // OS file chooser (#3) void setupRotary (juce::Slider&, juce::Label&, const juce::String& name, const juce::String& tip); void attachSlider (juce::Slider&, const char* id); void setupCombo (juce::ComboBox&, const char* id, const juce::String& tip); @@ -169,11 +170,12 @@ class AnamorphAudioProcessorEditor : public juce::AudioProcessorEditor, juce::Label settingsTitle; juce::Label persistLabel; // Persist moved into Settings as a bar (#21) - // Save-preset overlay (F2) + // Save-preset overlay (F2) + the OS Load chooser (#3) Backdrop savePresetBackdrop; juce::Label saveTitle; juce::TextEditor saveNameEditor; juce::TextButton saveOkButton { "Save" }, saveCancelButton { "Cancel" }; + std::unique_ptr fileChooser; juce::OwnedArray sliderAtts; juce::OwnedArray buttonAtts; @@ -199,8 +201,6 @@ class AnamorphAudioProcessorEditor : public juce::AudioProcessorEditor, // coarse timer tick is what stuttered (#6). Same ease curve, time-based. juce::VBlankAttachment meterVBlank; double lastFrameTime = 0.0; - float meterAnimFrom = 0.0f, meterAnimTarget = -1.0f; // ease-out reveal state (#3) - double meterAnimT = 1.0; // Micro-animation driver (F3): per-frame eased "hovA"/"actA"/"onA" component // properties the LookAndFeel blends with; repaints fire only while moving. diff --git a/src/PresetManager.cpp b/src/PresetManager.cpp index 8fddba0..c909a81 100644 --- a/src/PresetManager.cpp +++ b/src/PresetManager.cpp @@ -39,7 +39,7 @@ namespace return presets; } - constexpr const char* kPresetExt = ".anamorph"; + const juce::String kPresetExt = PresetManager::fileSuffix(); } // ---------------------------------------------------------------------------- @@ -147,6 +147,16 @@ void PresetManager::load (int index) sigAtLoad = soundSig(); } +bool PresetManager::loadFile (const juce::File& f) +{ + auto xml = juce::parseXML (f); + if (xml == nullptr) return false; + applySoundTree (juce::ValueTree::fromXml (*xml)); + current = f.getFileNameWithoutExtension(); + sigAtLoad = soundSig(); + return true; +} + void PresetManager::step (int delta) { if (list.isEmpty()) return; diff --git a/src/PresetManager.h b/src/PresetManager.h index 25b3039..2b137cb 100644 --- a/src/PresetManager.h +++ b/src/PresetManager.h @@ -35,6 +35,7 @@ class PresetManager // Win %APPDATA%/RollyTech/Anamorph/Presets // Linux ~/.config/RollyTech/Anamorph/Presets static juce::File presetDirectory(); + static juce::String fileSuffix() { return ".anamorph"; } // shared with the OS chooser filter (#3) void refresh(); // rescan the user folder const juce::Array& entries() const noexcept { return list; } @@ -44,6 +45,7 @@ class PresetManager bool isDirty() const; // sound edited since load/save void load (int index); // message thread only + bool loadFile (const juce::File&); // load an arbitrary .anamorph file (OS chooser, #3) void step (int delta); // prev/next with wrap-around bool saveUser (const juce::String& name); // write + select; false on IO error diff --git a/src/gui/LookAndFeel.cpp b/src/gui/LookAndFeel.cpp index ab6d4e3..7d54cae 100644 --- a/src/gui/LookAndFeel.cpp +++ b/src/gui/LookAndFeel.cpp @@ -27,6 +27,10 @@ void AnamorphLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int const auto bounds = juce::Rectangle ((float) x, (float) y, (float) w, (float) h).reduced (4.0f); const auto radius = juce::jmin (bounds.getWidth(), bounds.getHeight()) * 0.5f; const auto centre = bounds.getCentre(); + // Draw at the EASED visual position when the micro-anim driver is publishing + // one (preset / A-B sweep, #5); falls back to the live position otherwise. + if (const auto* v = s.getProperties().getVarPointer ("vpos")) + pos = juce::jlimit (0.0f, 1.0f, (float) (double) *v); const auto angle = startAngle + pos * (endAngle - startAngle); const float thick = juce::jmax (3.0f, radius * 0.16f); @@ -572,6 +576,55 @@ void AnamorphLookAndFeel::positionComboBoxText (juce::ComboBox& box, juce::Label label.setFont (getComboBoxFont (box)); } +// Text fields carrying a "glow" property (the Save-Preset name box) get a +// rounded, faintly accent-lit border when focused -- the same micro-glow as an +// open combo, so it reads premium rather than a thin default rectangle (#11). +void AnamorphLookAndFeel::fillTextEditorBackground (juce::Graphics& g, int width, int height, + juce::TextEditor& ed) +{ + if (! (bool) ed.getProperties().getWithDefault ("glow", false)) + { + juce::LookAndFeel_V4::fillTextEditorBackground (g, width, height, ed); + return; + } + auto b = juce::Rectangle (0.0f, 0.0f, (float) width, (float) height).reduced (0.5f); + g.setColour (ed.findColour (juce::TextEditor::backgroundColourId)); + g.fillRoundedRectangle (b, 5.0f); +} + +void AnamorphLookAndFeel::drawTextEditorOutline (juce::Graphics& g, int width, int height, + juce::TextEditor& ed) +{ + if (! (bool) ed.getProperties().getWithDefault ("glow", false)) + { + juce::LookAndFeel_V4::drawTextEditorOutline (g, width, height, ed); // value boxes unchanged + return; + } + + auto b = juce::Rectangle (0.0f, 0.0f, (float) width, (float) height).reduced (0.5f); + const float radius = 5.0f; + + if (ed.hasKeyboardFocus (true)) + { + // Faint accent bloom just inside the rim + a thin vertical-gradient border. + for (int i = 3; i >= 1; --i) + { + g.setColour (colours::accent.withAlpha (0.045f * (float) i)); + g.drawRoundedRectangle (b.reduced ((float) (4 - i)), + juce::jmax (1.5f, radius - (float) (4 - i)), 1.8f); + } + juce::ColourGradient og (colours::accent.brighter (0.20f), b.getX(), b.getY(), + colours::accent.withAlpha (0.55f), b.getX(), b.getBottom(), false); + g.setGradientFill (og); + g.drawRoundedRectangle (b, radius, 1.2f); + } + else + { + g.setColour (colours::outline); + g.drawRoundedRectangle (b, radius, 1.0f); + } +} + juce::Font AnamorphLookAndFeel::getLabelFont (juce::Label&) { return juce::Font (juce::FontOptions (13.0f)); diff --git a/src/gui/LookAndFeel.h b/src/gui/LookAndFeel.h index 8b878a5..7f45061 100644 --- a/src/gui/LookAndFeel.h +++ b/src/gui/LookAndFeel.h @@ -122,6 +122,11 @@ class AnamorphLookAndFeel : public juce::LookAndFeel_V4 // A value box you can drag (up/down) to change the value, like the knob (#2). juce::Label* createSliderTextBox (juce::Slider&) override; + // Focused text fields tagged with a "glow" property get the combo's subtle + // accent micro-glow instead of a plain hard outline (#11). + void drawTextEditorOutline (juce::Graphics&, int width, int height, juce::TextEditor&) override; + void fillTextEditorBackground (juce::Graphics&, int width, int height, juce::TextEditor&) override; + // Uniform, compact font for every combo + its pop-up list (#13). juce::Font getComboBoxFont (juce::ComboBox&) override; juce::Font getPopupMenuFont() override;