diff --git a/cmake/yup_modules.cmake b/cmake/yup_modules.cmake index 92f41ad74..f7e95c20d 100644 --- a/cmake/yup_modules.cmake +++ b/cmake/yup_modules.cmake @@ -624,30 +624,24 @@ macro (yup_add_default_modules modules_path) set (modules_group "Modules") yup_add_module (${modules_path}/modules/yup_core "${modules_definitions}" ${modules_group}) add_library (yup::yup_core ALIAS yup_core) - yup_add_module (${modules_path}/modules/yup_events "${modules_definitions}" ${modules_group}) add_library (yup::yup_events ALIAS yup_events) - yup_add_module (${modules_path}/modules/yup_data_model "${modules_definitions}" ${modules_group}) add_library (yup::yup_data_model ALIAS yup_data_model) - yup_add_module (${modules_path}/modules/yup_audio_basics "${modules_definitions}" ${modules_group}) add_library (yup::yup_audio_basics ALIAS yup_audio_basics) - yup_add_module (${modules_path}/modules/yup_audio_devices "${modules_definitions}" ${modules_group}) add_library (yup::yup_audio_devices ALIAS yup_audio_devices) - yup_add_module (${modules_path}/modules/yup_audio_processors "${modules_definitions}" ${modules_group}) add_library (yup::yup_audio_processors ALIAS yup_audio_processors) - yup_add_module (${modules_path}/modules/yup_audio_plugin_client "${modules_definitions}" ${modules_group}) add_library (yup::yup_audio_plugin_client ALIAS yup_audio_plugin_client) - yup_add_module (${modules_path}/modules/yup_graphics "${modules_definitions}" ${modules_group}) add_library (yup::yup_graphics ALIAS yup_graphics) - yup_add_module (${modules_path}/modules/yup_gui "${modules_definitions}" ${modules_group}) add_library (yup::yup_gui ALIAS yup_gui) + yup_add_module (${modules_path}/modules/yup_audio_gui "${modules_definitions}" ${modules_group}) + add_library (yup::yup_audio_gui ALIAS yup_audio_gui) if (YUP_ARG_ENABLE_PYTHON) if (NOT YUP_BUILD_WHEEL) diff --git a/examples/graphics/CMakeLists.txt b/examples/graphics/CMakeLists.txt index 74df7eeb8..a48e5a326 100644 --- a/examples/graphics/CMakeLists.txt +++ b/examples/graphics/CMakeLists.txt @@ -69,6 +69,7 @@ yup_standalone_app ( yup::yup_events yup::yup_graphics yup::yup_gui + yup::yup_audio_gui yup::yup_audio_processors libpng libwebp diff --git a/examples/graphics/source/examples/Audio.h b/examples/graphics/source/examples/Audio.h index 34c60fbe9..dc3e92b1e 100644 --- a/examples/graphics/source/examples/Audio.h +++ b/examples/graphics/source/examples/Audio.h @@ -26,10 +26,10 @@ //============================================================================== -class SineWaveGenerator +class HarmonicSineGenerator { public: - SineWaveGenerator() + HarmonicSineGenerator() : sampleRate (44100.0) , currentAngle (0.0) , frequency (0.0) @@ -40,17 +40,13 @@ class SineWaveGenerator void setSampleRate (double newSampleRate) { sampleRate = newSampleRate; - - frequency.reset (newSampleRate, 0.1); - amplitude.reset (newSampleRate, 0.1); + frequency.reset (newSampleRate, 0.05); + amplitude.reset (newSampleRate, 0.02); } - void setFrequency (double newFrequency, bool immediate = false) + void setFrequency (double newFrequency) { - if (immediate) - frequency.setCurrentAndTargetValue ((yup::MathConstants::twoPi * newFrequency) / sampleRate); - else - frequency.setTargetValue ((yup::MathConstants::twoPi * newFrequency) / sampleRate); + frequency.setTargetValue ((yup::MathConstants::twoPi * newFrequency) / sampleRate); } void setAmplitude (float newAmplitude) @@ -58,7 +54,7 @@ class SineWaveGenerator amplitude.setTargetValue (newAmplitude); } - float getAmplitude() const + float getCurrentAmplitude() const { return amplitude.getCurrentValue(); } @@ -83,6 +79,161 @@ class SineWaveGenerator //============================================================================== +class HarmonicSynth +{ +public: + HarmonicSynth() + : isNoteOn (false) + , currentNote (-1) + , fundamentalFrequency (0.0) + , masterAmplitude (0.5f) + { + // Initialize harmonic generators + const int numHarmonics = 16; // 4x4 grid + harmonicGenerators.resize (numHarmonics); + harmonicMultipliers.resize (numHarmonics); + harmonicAmplitudes.resize (numHarmonics); + + for (int i = 0; i < numHarmonics; ++i) + { + harmonicGenerators[i] = std::make_unique(); + + // Set up harmonic relationships (1st, 2nd, 3rd harmonic, etc., plus some non-integer ratios) + if (i < 8) + harmonicMultipliers[i] = (i + 1); // 1x, 2x, 3x, 4x, 5x, 6x, 7x, 8x + else + harmonicMultipliers[i] = (i - 7) * 0.5 + 0.5; // 0.5x, 1x, 1.5x, 2x, 2.5x, 3x, 3.5x, 4x + + harmonicAmplitudes[i] = 0.0f; // Start silent + } + } + + void setSampleRate (double newSampleRate) + { + for (auto& generator : harmonicGenerators) + generator->setSampleRate (newSampleRate); + } + + void noteOn (int midiNoteNumber, float velocity) + { + currentNote = midiNoteNumber; + isNoteOn = true; + + // Convert MIDI note to frequency: f = 440 * 2^((n-69)/12) + fundamentalFrequency = 440.0 * std::pow (2.0, (midiNoteNumber - 69) / 12.0); + + updateHarmonicFrequencies(); + updateHarmonicAmplitudes (velocity); + } + + void noteOff (int midiNoteNumber) + { + if (currentNote == midiNoteNumber) + { + isNoteOn = false; + for (auto& generator : harmonicGenerators) + generator->setAmplitude (0.0f); + } + } + + void allNotesOff() + { + isNoteOn = false; + currentNote = -1; + for (auto& generator : harmonicGenerators) + generator->setAmplitude (0.0f); + } + + void setHarmonicAmplitude (int harmonicIndex, float amplitude) + { + if (harmonicIndex >= 0 && harmonicIndex < harmonicAmplitudes.size()) + { + harmonicAmplitudes[harmonicIndex] = amplitude; + if (isNoteOn) + updateHarmonicAmplitudes (1.0f); // Use current velocity + } + } + + void setMasterAmplitude (float newAmplitude) + { + masterAmplitude = newAmplitude; + if (isNoteOn) + updateHarmonicAmplitudes (1.0f); + } + + float getMasterAmplitude() const + { + return masterAmplitude; + } + + bool isPlaying() const + { + if (! isNoteOn) + return false; + + for (const auto& generator : harmonicGenerators) + { + if (generator->getCurrentAmplitude() > 0.001f) + return true; + } + return false; + } + + int getCurrentNote() const + { + return currentNote; + } + + float getNextSample() + { + float mixedSample = 0.0f; + + for (auto& generator : harmonicGenerators) + { + mixedSample += generator->getNextSample(); + } + + return mixedSample * masterAmplitude; + } + + double getHarmonicMultiplier (int index) const + { + if (index >= 0 && index < harmonicMultipliers.size()) + return harmonicMultipliers[index]; + return 1.0; + } + +private: + void updateHarmonicFrequencies() + { + for (size_t i = 0; i < harmonicGenerators.size(); ++i) + { + double harmonicFreq = fundamentalFrequency * harmonicMultipliers[i]; + harmonicGenerators[i]->setFrequency (harmonicFreq); + } + } + + void updateHarmonicAmplitudes (float velocity) + { + for (size_t i = 0; i < harmonicGenerators.size(); ++i) + { + float amplitude = harmonicAmplitudes[i] * velocity * masterAmplitude; + harmonicGenerators[i]->setAmplitude (amplitude); + } + } + + std::vector> harmonicGenerators; + std::vector harmonicMultipliers; + std::vector harmonicAmplitudes; + + bool isNoteOn; + int currentNote; + double fundamentalFrequency; + float masterAmplitude; +}; + +//============================================================================== + class Oscilloscope : public yup::Component { public: @@ -156,83 +307,201 @@ class Oscilloscope : public yup::Component class AudioExample : public yup::Component , public yup::AudioIODeviceCallback + , public yup::MidiKeyboardState::Listener { public: AudioExample() : Component ("AudioExample") + , keyboardComponent (keyboardState, yup::MidiKeyboardComponent::horizontalKeyboard) { // Initialize the audio device deviceManager.initialiseWithDefaultDevices (0, 2); - // Initialize sine wave generators + // Initialize harmonic synthesizer double sampleRate = deviceManager.getAudioDeviceSetup().sampleRate; - sineWaveGenerators.resize (totalRows * totalColumns); - for (size_t i = 0; i < sineWaveGenerators.size(); ++i) - { - sineWaveGenerators[i] = std::make_unique(); - sineWaveGenerators[i]->setSampleRate (sampleRate); - sineWaveGenerators[i]->setFrequency (440.0 * std::pow (1.1, i), true); - } - - // Add sliders + harmonicSynth.setSampleRate (sampleRate); + + // Set up MIDI keyboard + keyboardState.addListener (this); + keyboardComponent.setAvailableRange (36, 84); // C2 to C6 + keyboardComponent.setLowestVisibleKey (48); // Start from C3 + keyboardComponent.setMidiChannel (1); + keyboardComponent.setVelocity (0.7f); + addAndMakeVisible (keyboardComponent); + + // Create title and subtitle labels + titleLabel = std::make_unique ("Title"); + titleLabel->setText ("YUP Harmonic Synthesizer"); + //titleLabel->setJustification (yup::Justification::centred); + //titleLabel->setFont (16.0f); + titleLabel->setColor (yup::Label::Style::textFillColorId, yup::Colors::white); + addAndMakeVisible (*titleLabel); + + subtitleLabel = std::make_unique ("Subtitle"); + subtitleLabel->setText ("Each knob controls a harmonic of the played note - experiment to create rich tones!"); + //subtitleLabel->setJustification (yup::Justification::centred); + //subtitleLabel->setFont (12.0f); + subtitleLabel->setColor (yup::Label::Style::textFillColorId, yup::Colors::white); + addAndMakeVisible (*subtitleLabel); + + // Create note indicator label + noteIndicatorLabel = std::make_unique ("NoteIndicator"); + noteIndicatorLabel->setText (""); + //noteIndicatorLabel->setJustification (yup::Justification::centred); + //noteIndicatorLabel->setFont (12.0f); + noteIndicatorLabel->setColor (yup::Label::Style::textFillColorId, yup::Colors::black); + noteIndicatorLabel->setColor (yup::Label::Style::backgroundColorId, yup::Colors::yellow.withAlpha (0.8f)); + addChildComponent (*noteIndicatorLabel); + + auto font = yup::ApplicationTheme::getGlobalTheme()->getDefaultFont(); + + // Add harmonic control sliders (4x4 grid) for (int i = 0; i < totalRows * totalColumns; ++i) { - auto slider = sliders.add (std::make_unique (yup::String (i))); + auto slider = sliders.add (std::make_unique (yup::Slider::RotaryVerticalDrag)); + + // Configure slider range and default value + slider->setRange (0.0f, 1.0f); + slider->setDefaultValue (0.0f); - slider->onValueChanged = [this, i, sampleRate] (float value) + slider->onValueChanged = [this, i] (float value) { - sineWaveGenerators[i]->setFrequency (440.0 * std::pow (1.1, i + value)); - sineWaveGenerators[i]->setAmplitude (value * 0.5); + harmonicSynth.setHarmonicAmplitude (i, value * 0.4f); // Scale down to prevent clipping }; addAndMakeVisible (slider); + + // Create harmonic labels for each slider + auto label = harmonicLabels.add (std::make_unique (yup::String ("HarmonicLabel") + yup::String (i))); + //label->setJustificationType (yup::Justification::centred); + //label->setFont (10.0f); + label->setColor (yup::Label::Style::textFillColorId, yup::Colors::lightgray); + label->setFont (font.withHeight (8.0f)); + + // Set the harmonic multiplier text + auto multiplier = harmonicSynth.getHarmonicMultiplier (i); + label->setText (yup::String (multiplier, 1) + "x", yup::dontSendNotification); + + addAndMakeVisible (*label); } // Add buttons - button = std::make_unique ("Randomize"); - button->onClick = [this] + randomizeButton = std::make_unique ("Randomize"); + randomizeButton->onClick = [this] { for (int i = 0; i < sliders.size(); ++i) sliders[i]->setValue (yup::Random::getSystemRandom().nextFloat()); }; - addAndMakeVisible (*button); + addAndMakeVisible (*randomizeButton); + + // Add clear all notes button + clearButton = std::make_unique ("All Notes Off"); + clearButton->onClick = [this] + { + keyboardState.allNotesOff (0); // Turn off all notes on all channels + harmonicSynth.allNotesOff(); + }; + addAndMakeVisible (*clearButton); + + // Add volume control + volumeSlider = std::make_unique (yup::Slider::LinearBarHorizontal, "Volume"); + + // Configure slider range and default value + volumeSlider->setRange ({ 0.0f, 1.0f }); + volumeSlider->setDefaultValue (0.5f); + + volumeSlider->onValueChanged = [this] (float value) + { + masterVolume = value; + }; + volumeSlider->setValue (0.5f); // Set initial volume to 50% + addAndMakeVisible (*volumeSlider); // Add the oscilloscope addAndMakeVisible (oscilloscope); + + // Set some initial harmonic values for a nice sound + if (sliders.size() >= 4) + { + sliders[0]->setValue (0.8f); // Fundamental + sliders[1]->setValue (0.4f); // 2nd harmonic + sliders[2]->setValue (0.2f); // 3rd harmonic + sliders[3]->setValue (0.1f); // 4th harmonic + } } ~AudioExample() override { + keyboardState.removeListener (this); deviceManager.removeAudioCallback (this); deviceManager.closeAudioDevice(); } void resized() override { - auto bounds = getLocalBounds().reduced (proportionOfWidth (0.1f), proportionOfHeight (0.2f)); - auto width = bounds.getWidth() / totalColumns; - auto height = bounds.getHeight() / totalRows; + auto bounds = getLocalBounds(); + + // Title area at the top + auto titleHeight = proportionOfHeight (0.05f); + auto titleBounds = bounds.removeFromTop (titleHeight); + titleLabel->setBounds (titleBounds); - for (int i = 0; i < totalRows && sliders.size(); ++i) + // Subtitle area + auto subtitleHeight = proportionOfHeight (0.03f); + auto subtitleBounds = bounds.removeFromTop (subtitleHeight); + subtitleLabel->setBounds (subtitleBounds); + + // Reserve space for MIDI keyboard at the bottom + auto keyboardHeight = proportionOfHeight (0.20f); + auto keyboardBounds = bounds.removeFromBottom (keyboardHeight); + keyboardComponent.setBounds (keyboardBounds.reduced (proportionOfWidth (0.02f), proportionOfHeight (0.01f))); + + // Reserve space for oscilloscope above the keyboard + auto oscilloscopeHeight = proportionOfHeight (0.2f); + auto oscilloscopeBounds = bounds.removeFromBottom (oscilloscopeHeight); + oscilloscope.setBounds (oscilloscopeBounds.reduced (proportionOfWidth (0.01f), proportionOfHeight (0.01f))); + + // Reserve space for buttons area + auto buttonHeight = proportionOfHeight (0.08f); + auto buttonArea = bounds.removeFromBottom (buttonHeight); + + auto buttonWidth = buttonArea.getWidth() / 3; + if (randomizeButton != nullptr) + randomizeButton->setBounds (buttonArea.removeFromLeft (buttonWidth).reduced (proportionOfWidth (0.01f), proportionOfHeight (0.01f))); + + if (clearButton != nullptr) + clearButton->setBounds (buttonArea.removeFromLeft (buttonWidth).reduced (proportionOfWidth (0.01f), proportionOfHeight (0.01f))); + + if (volumeSlider != nullptr) + volumeSlider->setBounds (buttonArea.removeFromLeft (buttonWidth).reduced (proportionOfWidth (0.01f), proportionOfHeight (0.01f))); + + // Use remaining space for harmonic control sliders with labels + auto sliderBounds = bounds.reduced (proportionOfWidth (0.05f), proportionOfHeight (0.02f)); + auto width = sliderBounds.getWidth() / totalColumns; + auto height = sliderBounds.getHeight() / totalRows; + + for (int i = 0; i < totalRows && i * totalColumns < sliders.size(); ++i) { - auto row = bounds.removeFromTop (height); - for (int j = 0; j < totalColumns; ++j) + auto row = sliderBounds.removeFromTop (height); + for (int j = 0; j < totalColumns && i * totalColumns + j < sliders.size(); ++j) { auto col = row.removeFromLeft (width); - sliders.getUnchecked (i * totalRows + j)->setBounds (col.largestFittingSquare()); - } - } + auto harmonicIndex = i * totalColumns + j; - if (button != nullptr) - button->setBounds (getLocalBounds() - .removeFromTop (proportionOfHeight (0.2f)) - .reduced (proportionOfWidth (0.2f), 0.0f)); + // Reserve space for label at bottom of column + auto labelHeight = 10; + auto labelBounds = col.removeFromBottom (labelHeight); + harmonicLabels[harmonicIndex]->setBounds (labelBounds); - auto bottomBounds = getLocalBounds() - .removeFromBottom (proportionOfHeight (0.2f)) - .reduced (proportionOfWidth (0.01f), proportionOfHeight (0.01f)); + // Use remaining space for slider - make it rectangular for slider appearance + auto sliderArea = col.largestFittingSquare(); + sliders.getUnchecked (harmonicIndex)->setBounds (sliderArea); + } + } - oscilloscope.setBounds (bottomBounds); + // Position note indicator at bottom left + auto noteIndicatorBounds = yup::Rectangle (10, getHeight() - 40, 200, 30); + noteIndicatorLabel->setBounds (noteIndicatorBounds); } void paint (yup::Graphics& g) override @@ -255,6 +524,32 @@ class AudioExample if (oscilloscope.isVisible()) oscilloscope.repaint(); + + // Update note indicator + if (harmonicSynth.isPlaying()) + { + if (! noteIndicatorLabel->isVisible()) + { + noteIndicatorLabel->setVisible (true); + } + auto noteText = yup::String ("Playing Note: ") + yup::String (harmonicSynth.getCurrentNote()); + noteIndicatorLabel->setText (noteText, yup::dontSendNotification); + } + else + { + noteIndicatorLabel->setVisible (false); + } + } + + // MIDI keyboard event handlers + void handleNoteOn (yup::MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity) override + { + harmonicSynth.noteOn (midiNoteNumber, velocity); + } + + void handleNoteOff (yup::MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity) override + { + harmonicSynth.noteOff (midiNoteNumber); } void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, @@ -266,23 +561,22 @@ class AudioExample { for (int sample = 0; sample < numSamples; ++sample) { - float mixedSample = 0.0f; - float totalScale = 0.0f; + // Generate the next sample from the harmonic synth + float synthSample = harmonicSynth.getNextSample(); - for (int i = 0; i < sineWaveGenerators.size(); ++i) - { - mixedSample += sineWaveGenerators[i]->getNextSample(); - totalScale += sineWaveGenerators[i]->getAmplitude(); - } + // Apply master volume + synthSample *= masterVolume; - if (totalScale > 1.0f) - mixedSample /= static_cast (totalScale); + // Apply soft limiting to prevent clipping + synthSample = std::tanh (synthSample); + // Output to all channels for (int channel = 0; channel < numOutputChannels; ++channel) - outputChannelData[channel][sample] = mixedSample; + outputChannelData[channel][sample] = synthSample; + // Store for oscilloscope display auto pos = readPos.fetch_add (1); - inputData[pos] = mixedSample; + inputData[pos] = synthSample; readPos = readPos % inputData.size(); } @@ -311,17 +605,31 @@ class AudioExample private: yup::AudioDeviceManager deviceManager; - std::vector> sineWaveGenerators; + HarmonicSynth harmonicSynth; + + // MIDI keyboard components + yup::MidiKeyboardState keyboardState; + yup::MidiKeyboardComponent keyboardComponent; std::vector renderData; std::vector inputData; yup::CriticalSection renderMutex; std::atomic_int readPos = 0; + // UI Components + std::unique_ptr titleLabel; + std::unique_ptr subtitleLabel; + std::unique_ptr noteIndicatorLabel; + yup::OwnedArray sliders; + yup::OwnedArray harmonicLabels; int totalRows = 4; int totalColumns = 4; - std::unique_ptr button; + std::unique_ptr randomizeButton; + std::unique_ptr clearButton; + std::unique_ptr volumeSlider; Oscilloscope oscilloscope; + + float masterVolume = 0.5f; }; diff --git a/examples/graphics/source/examples/LayoutFonts.h b/examples/graphics/source/examples/LayoutFonts.h index de7fcee74..bd36a1e55 100644 --- a/examples/graphics/source/examples/LayoutFonts.h +++ b/examples/graphics/source/examples/LayoutFonts.h @@ -28,7 +28,7 @@ class LayoutFontsExample : public yup::Component public: LayoutFontsExample() : Component ("LayoutFontsExample") - , font (yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()) + , font (yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (16.0f)) { } @@ -126,7 +126,7 @@ class LayoutFontsExample : public yup::Component modifier.setWrap (wrap); modifier.clear(); - modifier.appendText (text, nullptr, font.getFont(), fontSize); + modifier.appendText (text, nullptr, font.withHeight (fontSize)); } }; diff --git a/examples/graphics/source/examples/Paths.h b/examples/graphics/source/examples/Paths.h index f0c8b9b8f..392b7f793 100644 --- a/examples/graphics/source/examples/Paths.h +++ b/examples/graphics/source/examples/Paths.h @@ -81,7 +81,7 @@ class PathsExample : public yup::Component auto modifier = text.startUpdate(); modifier.setMaxSize (area.getSize()); modifier.setHorizontalAlign (yup::StyledText::center); - modifier.appendText (title, yup::ApplicationTheme::getGlobalTheme()->getDefaultFont(), 12.0f); + modifier.appendText (title, yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (12.0f)); } g.setFillColor (yup::Colors::white); diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 50320aa3d..da80bb5c5 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -72,7 +72,7 @@ class PopupMenuDemo : public yup::Component auto styledText = yup::StyledText(); { auto modifier = styledText.startUpdate(); - modifier.appendText ("PopupMenu Features: Placement, Submenus, Scrolling", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); + modifier.appendText ("PopupMenu Features: Placement, Submenus, Scrolling", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (16.0f)); } g.setFillColor (yup::Color (0xffffffff)); diff --git a/examples/graphics/source/examples/SliderDemo.h b/examples/graphics/source/examples/SliderDemo.h new file mode 100644 index 000000000..b731ce1ec --- /dev/null +++ b/examples/graphics/source/examples/SliderDemo.h @@ -0,0 +1,214 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +class SliderDemo : public yup::Component +{ +public: + SliderDemo() + : Component ("SliderDemo") + { + setupSliders(); + setupLabels(); + } + +private: + void setupSliders() + { + // Horizontal Linear Slider + horizontalSlider = std::make_unique (yup::Slider::LinearHorizontal); + horizontalSlider->setRange (0.0, 100.0); + horizontalSlider->setValue (50.0); + horizontalSlider->onValueChanged = [this] (double value) + { + horizontalLabel->setText ("Horizontal: " + yup::String (value, 1), yup::dontSendNotification); + }; + addAndMakeVisible (horizontalSlider.get()); + + // Vertical Linear Slider + verticalSlider = std::make_unique (yup::Slider::LinearVertical); + verticalSlider->setRange (0.0, 100.0); + verticalSlider->setValue (30.0); + verticalSlider->onValueChanged = [this] (double value) + { + verticalLabel->setText ("Vertical: " + yup::String (value, 1), yup::dontSendNotification); + }; + addAndMakeVisible (verticalSlider.get()); + + // Rotary Slider (Horizontal Drag) + rotarySlider = std::make_unique (yup::Slider::RotaryHorizontalDrag); + rotarySlider->setRange (0.0, 100.0); + rotarySlider->setValue (70.0); + rotarySlider->onValueChanged = [this] (double value) + { + rotaryLabel->setText ("Rotary: " + yup::String (value, 1), yup::dontSendNotification); + }; + addAndMakeVisible (rotarySlider.get()); + + // Bar Horizontal Slider + barHorizontalSlider = std::make_unique (yup::Slider::LinearBarHorizontal); + barHorizontalSlider->setRange (0.0, 100.0); + barHorizontalSlider->setValue (75.0); + barHorizontalSlider->onValueChanged = [this] (double value) + { + barHorizontalLabel->setText ("Bar H: " + yup::String (value, 0) + "%", yup::dontSendNotification); + }; + addAndMakeVisible (barHorizontalSlider.get()); + + // Bar Vertical Slider + barVerticalSlider = std::make_unique (yup::Slider::LinearBarVertical); + barVerticalSlider->setRange (0.0, 10.0); + barVerticalSlider->setValue (6.0); + barVerticalSlider->onValueChanged = [this] (double value) + { + barVerticalLabel->setText ("Bar V: " + yup::String (value, 1), yup::dontSendNotification); + }; + addAndMakeVisible (barVerticalSlider.get()); + + // Two Value Horizontal Slider + twoValueSlider = std::make_unique (yup::Slider::TwoValueHorizontal); + twoValueSlider->setRange (0.0, 100.0); + twoValueSlider->setMinValue (25.0); + twoValueSlider->setMaxValue (75.0); + twoValueSlider->onValueChanged = [this] (double) + { + twoValueLabel->setText ("Range: " + yup::String (twoValueSlider->getMinValue(), 0) + "-" + yup::String (twoValueSlider->getMaxValue(), 0), yup::dontSendNotification); + }; + addAndMakeVisible (twoValueSlider.get()); + } + + void setupLabels() + { + // Title + titleLabel = std::make_unique ("title"); + titleLabel->setText ("YUP Slider Demo", yup::dontSendNotification); + addAndMakeVisible (titleLabel.get()); + + // Value labels + horizontalLabel = std::make_unique ("value1"); + horizontalLabel->setText ("Horizontal: 50.0", yup::dontSendNotification); + addAndMakeVisible (horizontalLabel.get()); + + verticalLabel = std::make_unique ("value2"); + verticalLabel->setText ("Vertical: 30.0", yup::dontSendNotification); + addAndMakeVisible (verticalLabel.get()); + + rotaryLabel = std::make_unique ("value3"); + rotaryLabel->setText ("Rotary: 70.0", yup::dontSendNotification); + addAndMakeVisible (rotaryLabel.get()); + + barHorizontalLabel = std::make_unique ("value4"); + barHorizontalLabel->setText ("Bar H: 75%", yup::dontSendNotification); + addAndMakeVisible (barHorizontalLabel.get()); + + barVerticalLabel = std::make_unique ("value5"); + barVerticalLabel->setText ("Bar V: 6.0", yup::dontSendNotification); + addAndMakeVisible (barVerticalLabel.get()); + + twoValueLabel = std::make_unique ("value6"); + twoValueLabel->setText ("Range: 25-75", yup::dontSendNotification); + addAndMakeVisible (twoValueLabel.get()); + } + + void resized() override + { + auto bounds = getLocalBounds(); + auto margin = 20.0f; + auto sliderHeight = 60.0f; + auto labelHeight = 25.0f; + auto spacing = 10.0f; + + auto y = margin; + + // Title + titleLabel->setBounds (margin, y, bounds.getWidth() - 2 * margin, 30.0f); + y += 40.0f; + + // Layout in a 2x3 grid + auto sliderWidth = (bounds.getWidth() - 3.0f * margin) / 2.0f; + auto columnHeight = sliderHeight + labelHeight + spacing; + + // Left column + horizontalSlider->setBounds (margin, y, sliderWidth, sliderHeight); + horizontalLabel->setBounds (margin, y + sliderHeight + 5.0f, sliderWidth, labelHeight); + + barHorizontalSlider->setBounds (margin, y + columnHeight, sliderWidth, sliderHeight); + barHorizontalLabel->setBounds (margin, y + columnHeight + sliderHeight + 5.0f, sliderWidth, labelHeight); + + twoValueSlider->setBounds (margin, y + 2.0f * columnHeight, sliderWidth, sliderHeight); + twoValueLabel->setBounds (margin, y + 2.0f * columnHeight + sliderHeight + 5.0f, sliderWidth, labelHeight); + + // Right column + auto rightX = margin + sliderWidth + margin; + + verticalSlider->setBounds (rightX, y, 80.0f, columnHeight); + verticalLabel->setBounds (rightX + 90.0f, y, sliderWidth - 90.0f, labelHeight); + + rotarySlider->setBounds (rightX, y + columnHeight, 80.0f, 80.0f); + rotaryLabel->setBounds (rightX + 90.0f, y + columnHeight, sliderWidth - 90.0f, labelHeight); + + barVerticalSlider->setBounds (rightX, y + 2.0f * columnHeight, 60.0f, columnHeight); + barVerticalLabel->setBounds (rightX + 70.0f, y + 2.0f * columnHeight, sliderWidth - 70.0f, labelHeight); + } + + void paint (yup::Graphics& g) override + { + g.setFillColor (yup::Color (0xff404040)); + g.fillAll(); + + // Draw section dividers + g.setStrokeColor (yup::Colors::gray.withAlpha (0.3f)); + g.setStrokeWidth (1.0f); + + auto bounds = getLocalBounds(); + auto margin = 20; + + // Horizontal line under title + g.strokeLine (margin, 70.0f, bounds.getWidth() - margin, 70.0f); + + // Vertical line separating columns + auto centerX = bounds.getWidth() / 2; + g.strokeLine (centerX, 80.0f, centerX, bounds.getHeight() - margin); + } + +private: + // Title + std::unique_ptr titleLabel; + + // Sliders + std::unique_ptr horizontalSlider; + std::unique_ptr verticalSlider; + std::unique_ptr rotarySlider; + std::unique_ptr barHorizontalSlider; + std::unique_ptr barVerticalSlider; + std::unique_ptr twoValueSlider; + + // Labels + std::unique_ptr horizontalLabel; + std::unique_ptr verticalLabel; + std::unique_ptr rotaryLabel; + std::unique_ptr barHorizontalLabel; + std::unique_ptr barVerticalLabel; + std::unique_ptr twoValueLabel; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SliderDemo) +}; diff --git a/examples/graphics/source/examples/VariableFonts.h b/examples/graphics/source/examples/VariableFonts.h index b5132538d..9c1752b53 100644 --- a/examples/graphics/source/examples/VariableFonts.h +++ b/examples/graphics/source/examples/VariableFonts.h @@ -59,7 +59,7 @@ class VariableFontsExample : public yup::Component label->setFont (font); addAndMakeVisible (label); - auto slider = sliders.add (std::make_unique (axisInfo->tagName)); + auto slider = sliders.add (std::make_unique (yup::Slider::Rotary, axisInfo->tagName)); slider->setDefaultValue (axisInfo->defaultValue); slider->setRange ({ axisInfo->minimumValue, axisInfo->maximumValue }); slider->setValue (axisInfo->defaultValue); @@ -92,7 +92,7 @@ class VariableFontsExample : public yup::Component modifier.setOverflow (yup::StyledText::visible); modifier.setWrap (yup::StyledText::wrap); modifier.clear(); - modifier.appendText (text, font.getFont(), fontSize); + modifier.appendText (text, font.withHeight (fontSize)); } bounds = bounds.reduced (10); @@ -164,7 +164,7 @@ class VariableFontsExample : public yup::Component label->setFont (font); addAndMakeVisible (label); - auto slider = sliders.add (std::make_unique (name)); + auto slider = sliders.add (std::make_unique (yup::Slider::Rotary, name)); slider->setDefaultValue (defaultValue); slider->setRange ({ minValue, maxValue }); slider->setValue (defaultValue); diff --git a/examples/graphics/source/examples/Widgets.h b/examples/graphics/source/examples/Widgets.h index 6e7e750ab..814cf4158 100644 --- a/examples/graphics/source/examples/Widgets.h +++ b/examples/graphics/source/examples/Widgets.h @@ -102,8 +102,8 @@ class WidgetsDemo : public yup::Component */ // Slider - slider = std::make_unique ("slider"); - slider->setRange (yup::Range (0.0f, 100.0f)); + slider = std::make_unique (yup::Slider::Rotary, "slider"); + slider->setRange (yup::Range (0.0, 100.0)); slider->setValue (50.0); slider->onValueChanged = [this] (float value) { diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index 1cd85118d..66666d59b 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #if YUP_MODULE_AVAILABLE_yup_python #include @@ -43,6 +44,7 @@ #include "examples/OpaqueDemo.h" #include "examples/Paths.h" #include "examples/PopupMenu.h" +#include "examples/SliderDemo.h" #include "examples/TextEditor.h" #include "examples/Svg.h" #include "examples/VariableFonts.h" @@ -108,6 +110,7 @@ class CustomWindow #endif registerDemo ("Popup Menu", counter++); registerDemo ("File Chooser", counter++); + registerDemo ("Sliders", counter++); registerDemo ("Widgets", counter++); registerDemo ("Artboard", counter++, [] (auto& artboard) { diff --git a/examples/plugin/source/ExampleEditor.cpp b/examples/plugin/source/ExampleEditor.cpp index b31ccd538..51f5238f0 100644 --- a/examples/plugin/source/ExampleEditor.cpp +++ b/examples/plugin/source/ExampleEditor.cpp @@ -26,7 +26,10 @@ class ExampleSlider : public yup::Slider { public: - using yup::Slider::Slider; + ExampleSlider() + : yup::Slider (yup::Slider::RotaryVerticalDrag) + { + } }; //============================================================================== @@ -35,7 +38,7 @@ ExampleEditor::ExampleEditor (ExamplePlugin& processor) : audioProcessor (processor) , gainParameter (audioProcessor.getParameters()[0]) { - x = std::make_unique ("Slider"); + x = std::make_unique(); x->setMouseCursor (yup::MouseCursor::Hand); x->setValue (gainParameter->getValue()); x->onDragStart = [this] (const yup::MouseEvent&) diff --git a/modules/yup_audio_gui/keyboard/yup_MidiKeyboardComponent.cpp b/modules/yup_audio_gui/keyboard/yup_MidiKeyboardComponent.cpp new file mode 100644 index 000000000..3b1e38f5d --- /dev/null +++ b/modules/yup_audio_gui/keyboard/yup_MidiKeyboardComponent.cpp @@ -0,0 +1,508 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +namespace +{ + +//============================================================================== + + + +} // namespace + +//============================================================================== +// Color identifiers +const Identifier MidiKeyboardComponent::Style::whiteKeyColorId ("midiKeyboardWhiteKey"); +const Identifier MidiKeyboardComponent::Style::whiteKeyPressedColorId ("midiKeyboardWhiteKeyPressed"); +const Identifier MidiKeyboardComponent::Style::whiteKeyShadowColorId ("midiKeyboardWhiteKeyShadow"); +const Identifier MidiKeyboardComponent::Style::blackKeyColorId ("midiKeyboardBlackKey"); +const Identifier MidiKeyboardComponent::Style::blackKeyPressedColorId ("midiKeyboardBlackKeyPressed"); +const Identifier MidiKeyboardComponent::Style::blackKeyShadowColorId ("midiKeyboardBlackKeyShadow"); +const Identifier MidiKeyboardComponent::Style::keyOutlineColorId ("midiKeyboardKeyOutline"); + +//============================================================================== +MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& stateToUse, Orientation orientationToUse) + : state (stateToUse), + orientation (orientationToUse) +{ + state.addListener (this); + setWantsKeyboardFocus (true); + //setMouseClickGrabsKeyboardFocus (true); +} + +MidiKeyboardComponent::~MidiKeyboardComponent() +{ + state.removeListener (this); +} + +//============================================================================== +void MidiKeyboardComponent::setVelocity (float newVelocity) +{ + velocity = jlimit (0.0f, 1.0f, newVelocity); +} + +void MidiKeyboardComponent::setMidiChannel (int midiChannelNumber) +{ + jassert (midiChannelNumber > 0 && midiChannelNumber <= 16); + + if (midiChannel != midiChannelNumber) + { + resetAnyKeysInUse(); + midiChannel = midiChannelNumber; + } +} + +void MidiKeyboardComponent::setOctaveForMiddleC (int octaveNumber) +{ + octaveNumForMiddleC = octaveNumber; + repaint(); +} + +void MidiKeyboardComponent::setLowestVisibleKey (int noteNumber) +{ + setAvailableRange (noteNumber, rangeEnd); +} + +void MidiKeyboardComponent::setAvailableRange (int lowestNote, int highestNote) +{ + jassert (lowestNote >= 0 && lowestNote <= 127); + jassert (highestNote >= 0 && highestNote <= 127); + jassert (lowestNote <= highestNote); + + if (rangeStart != lowestNote || rangeEnd != highestNote) + { + rangeStart = jlimit (0, 127, lowestNote); + rangeEnd = jlimit (0, 127, highestNote); + repaint(); + } +} + +//============================================================================== +Rectangle MidiKeyboardComponent::getRectangleForKey (int midiNoteNumber) const +{ + jassert (midiNoteNumber >= 0 && midiNoteNumber < 128); + + if (midiNoteNumber < rangeStart || midiNoteNumber > rangeEnd) + return {}; + + auto keyWidth = getKeyStartRange().getLength() / getNumWhiteKeysInRange (rangeStart, rangeEnd + 1); + Rectangle pos; + bool isBlack; + + getKeyPosition (midiNoteNumber, keyWidth, pos, isBlack); + + return pos; +} + +int MidiKeyboardComponent::getNoteAtPosition (Point position) const +{ + float mousePositionVelocity; + return remappedXYToNote (position, mousePositionVelocity); +} + +//============================================================================== +void MidiKeyboardComponent::paint (Graphics& g) +{ + if (auto style = ApplicationTheme::findComponentStyle (*this)) + style->paint (g, *ApplicationTheme::getGlobalTheme(), *this); +} + +//============================================================================== +void MidiKeyboardComponent::mouseDown (const MouseEvent& e) +{ + if (! isEnabled()) + return; + + updateNoteUnderMouse (e, true); + shouldCheckState = true; +} + +void MidiKeyboardComponent::mouseDrag (const MouseEvent& e) +{ + if (! isEnabled()) + return; + + updateNoteUnderMouse (e, true); +} + +void MidiKeyboardComponent::mouseUp (const MouseEvent& e) +{ + if (! isEnabled()) + return; + + // Always release all notes that were triggered by mouse interaction + for (auto noteDown : mouseDownNotes) + state.noteOff (midiChannel, noteDown, velocity); + + mouseDownNotes.clear(); + + // Update visual state to show keys are no longer pressed + updateNoteUnderMouse (e, false); + updateShadowNoteUnderMouse (e); + shouldCheckState = true; +} + +void MidiKeyboardComponent::mouseMove (const MouseEvent& e) +{ + if (! isEnabled()) + return; + + updateShadowNoteUnderMouse (e); +} + +void MidiKeyboardComponent::mouseEnter (const MouseEvent& e) +{ + updateShadowNoteUnderMouse (e); + + // If we're entering while dragging, trigger the note under the mouse + if (e.isAnyButtonDown()) + { + updateNoteUnderMouse (e, true); + } +} + +void MidiKeyboardComponent::mouseExit (const MouseEvent& e) +{ + updateShadowNoteUnderMouse (e); + + // If we're dragging and leaving the component, release all notes + if (e.isAnyButtonDown() && ! mouseDownNotes.isEmpty()) + { + for (auto noteDown : mouseDownNotes) + state.noteOff (midiChannel, noteDown, velocity); + + mouseDownNotes.clear(); + } +} + +void MidiKeyboardComponent::mouseWheel (const MouseEvent&, const MouseWheelData& wheel) +{ + const auto amount = (orientation == horizontalKeyboard && wheel.getDeltaX() != 0) + ? wheel.getDeltaX() + : (orientation != horizontalKeyboard && wheel.getDeltaY() != 0) + ? wheel.getDeltaY() : wheel.getDeltaX(); + + setLowestVisibleKey (rangeStart + roundToInt (amount * 5.0f)); +} + +//============================================================================== +void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int midiChannelNumber, int midiNoteNumber, float) +{ + if (midiInChannelMask & (1 << (midiChannelNumber - 1))) + repaintNote (midiNoteNumber); +} + +void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int midiChannelNumber, int midiNoteNumber, float) +{ + if (midiInChannelMask & (1 << (midiChannelNumber - 1))) + repaintNote (midiNoteNumber); +} + +//============================================================================== +void MidiKeyboardComponent::resized() +{ + shouldCheckState = true; +} + +void MidiKeyboardComponent::keyDown (const KeyPress& key, const Point& position) +{ + int midiNote = -1; + + if (key == KeyPress ('z')) midiNote = 0; + else if (key == KeyPress ('s')) midiNote = 1; + else if (key == KeyPress ('x')) midiNote = 2; + else if (key == KeyPress ('d')) midiNote = 3; + else if (key == KeyPress ('c')) midiNote = 4; + else if (key == KeyPress ('v')) midiNote = 5; + else if (key == KeyPress ('g')) midiNote = 6; + else if (key == KeyPress ('b')) midiNote = 7; + else if (key == KeyPress ('h')) midiNote = 8; + else if (key == KeyPress ('n')) midiNote = 9; + else if (key == KeyPress ('j')) midiNote = 10; + else if (key == KeyPress ('m')) midiNote = 11; + else if (key == KeyPress (',')) midiNote = 12; + else if (key == KeyPress ('l')) midiNote = 13; + else if (key == KeyPress ('.')) midiNote = 14; + else if (key == KeyPress (';')) midiNote = 15; + else if (key == KeyPress ('/')) midiNote = 16; + + if (midiNote >= 0) + { + midiNote += 12 * octaveNumForMiddleC; + + if (midiNote >= 0 && midiNote < 128) + state.noteOn (midiChannel, midiNote, velocity); + } +} + +void MidiKeyboardComponent::focusLost() +{ + resetAnyKeysInUse(); +} + +//============================================================================== +bool MidiKeyboardComponent::isNoteOn (int midiNoteNumber) const +{ + return state.isNoteOnForChannels (midiInChannelMask, midiNoteNumber); +} + +//============================================================================== +bool MidiKeyboardComponent::isBlackKey (int midiNoteNumber) const +{ + return MidiMessage::isMidiNoteBlack (midiNoteNumber); +} + +int MidiKeyboardComponent::getNumWhiteKeysInRange (int rangeStart, int rangeEnd) const +{ + int numWhiteKeys = 0; + + for (int i = rangeStart; i < rangeEnd; ++i) + if (! isBlackKey (i)) + ++numWhiteKeys; + + return numWhiteKeys; +} + +String MidiKeyboardComponent::getWhiteNoteText (int midiNoteNumber) +{ + if (isBlackKey (midiNoteNumber)) + return {}; + + static const char* const noteNames[] = { "C", "", "D", "", "E", "F", "", "G", "", "A", "", "B" }; + + return String (noteNames [midiNoteNumber % 12]); +} + +void MidiKeyboardComponent::getKeyPosition (int midiNoteNumber, float keyWidth, Rectangle& keyPos, bool& isBlack) const +{ + jassert (midiNoteNumber >= 0 && midiNoteNumber < 128); + + // Fixed black key offsets for proper positioning + // static const float blackKeyOffsets[] = { 0.0f, 0.25f, 0.0f, 0.35f, 0.0f, 0.0f, 0.25f, 0.0f, 0.3f, 0.0f, 0.35f, 0.0f }; + static const float blackKeyOffsets[] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; + + auto octave = midiNoteNumber / 12; + auto note = midiNoteNumber % 12; + + auto numWhiteKeysBefore = 0; + auto notePos = 0; + + for (int i = 0; i < note; ++i) + { + if (! isBlackKey (i)) + ++numWhiteKeysBefore; + } + + for (int i = rangeStart; i < midiNoteNumber; ++i) + { + if (! isBlackKey (i)) + ++notePos; + } + + isBlack = isBlackKey (midiNoteNumber); + + auto x = notePos * keyWidth; + auto w = keyWidth; + + if (isBlack) + { + auto blackKeyWidth = keyWidth * 0.7f; + x = x - (blackKeyWidth * 0.5f) + (keyWidth * blackKeyOffsets[note]); + w = blackKeyWidth; + } + + switch (orientation) + { + case horizontalKeyboard: + keyPos = Rectangle (x, 0.0f, w, (float) getHeight()); + break; + + case verticalKeyboardFacingLeft: + keyPos = Rectangle ((float) getWidth() - ((isBlack ? 0.7f : 1.0f) * (float) getWidth()), + x, (isBlack ? 0.7f : 1.0f) * (float) getWidth(), w); + break; + + case verticalKeyboardFacingRight: + keyPos = Rectangle (0.0f, (float) getHeight() - x - w, + (isBlack ? 0.7f : 1.0f) * (float) getWidth(), w); + break; + + default: + break; + } + + if (isBlack) + { + switch (orientation) + { + case horizontalKeyboard: keyPos = keyPos.withHeight (keyPos.getHeight() * 0.6f); break; + case verticalKeyboardFacingLeft: keyPos = keyPos.withWidth (keyPos.getWidth() * 0.6f); break; + case verticalKeyboardFacingRight: keyPos = keyPos.withX (keyPos.getX() + keyPos.getWidth() * 0.4f) + .withWidth (keyPos.getWidth() * 0.6f); break; + default: break; + } + } +} + +Range MidiKeyboardComponent::getKeyStartRange() const +{ + return (orientation == horizontalKeyboard) ? Range (0.0f, (float) getWidth()) + : Range (0.0f, (float) getHeight()); +} + +int MidiKeyboardComponent::xyToNote (Point pos, float& mousePositionVelocity) +{ + return remappedXYToNote (pos, mousePositionVelocity); +} + +int MidiKeyboardComponent::remappedXYToNote (Point pos, float& mousePositionVelocity) const +{ + auto keyWidth = getKeyStartRange().getLength() / getNumWhiteKeysInRange (rangeStart, rangeEnd + 1); + + auto coord = (orientation == horizontalKeyboard) ? pos.getX() : pos.getY(); + auto otherCoord = (orientation == horizontalKeyboard) ? pos.getY() : pos.getX(); + + auto blackKeyDepth = 0.7f; + + switch (orientation) + { + case horizontalKeyboard: blackKeyDepth = getHeight() * 0.6f; break; + case verticalKeyboardFacingLeft: blackKeyDepth = getWidth() * 0.6f; break; + case verticalKeyboardFacingRight: blackKeyDepth = getWidth() * 0.6f; break; + default: break; + } + + // First try black keys + for (int note = rangeStart; note <= rangeEnd; ++note) + { + if (isBlackKey (note)) + { + Rectangle area; + bool isBlack; + getKeyPosition (note, keyWidth, area, isBlack); + + if (area.contains (pos)) + { + mousePositionVelocity = jlimit (0.0f, 1.0f, otherCoord / area.getHeight()); + return note; + } + } + } + + // Then try white keys + for (int note = rangeStart; note <= rangeEnd; ++note) + { + if (! isBlackKey (note)) + { + Rectangle area; + bool isBlack; + getKeyPosition (note, keyWidth, area, isBlack); + + if (area.contains (pos)) + { + mousePositionVelocity = jlimit (0.0f, 1.0f, otherCoord / area.getHeight()); + return note; + } + } + } + + mousePositionVelocity = velocity; + return -1; +} + +void MidiKeyboardComponent::repaintNote (int midiNoteNumber) +{ + if (midiNoteNumber >= rangeStart && midiNoteNumber <= rangeEnd) + repaint (getRectangleForKey (midiNoteNumber).roundToInt().enlarged (1)); // getSmallestIntegerContainer +} + +void MidiKeyboardComponent::updateNoteUnderMouse (Point pos, bool isDown, int fingerNum) +{ + float mousePositionVelocity; + auto newNote = xyToNote (pos, mousePositionVelocity); + auto oldNote = mouseOverNote; + + // Always update hover visual state when the note under mouse changes + if (oldNote != newNote) + { + repaintNote (oldNote); + repaintNote (newNote); + mouseOverNote = newNote; + } + + if (isDown) + { + // Handle note triggering - this should work regardless of hover state + + // First, release any previously pressed notes that are no longer under the mouse + for (int i = mouseDownNotes.size(); --i >= 0;) + { + auto pressedNote = mouseDownNotes.getUnchecked (i); + if (pressedNote != newNote) + { + state.noteOff (midiChannel, pressedNote, mousePositionVelocity); + mouseDownNotes.remove (i); + } + } + + // Then, trigger the new note if it's valid and not already pressed + if (newNote >= 0 && ! mouseDownNotes.contains (newNote)) + { + state.noteOn (midiChannel, newNote, mousePositionVelocity); + mouseDownNotes.add (newNote); + } + } +} + +void MidiKeyboardComponent::updateNoteUnderMouse (const MouseEvent& e, bool isDown) +{ + updateNoteUnderMouse (e.getPosition(), isDown, 0); +} + +void MidiKeyboardComponent::resetAnyKeysInUse() +{ + if (! mouseDownNotes.isEmpty()) + { + for (auto noteDown : mouseDownNotes) + state.noteOff (midiChannel, noteDown, velocity); + + mouseDownNotes.clear(); + } + + mouseOverNote = -1; +} + +void MidiKeyboardComponent::updateShadowNoteUnderMouse (const MouseEvent& e) +{ + auto note = getNoteAtPosition (e.getPosition()); + + if (note != mouseOverNote) + { + repaintNote (mouseOverNote); + mouseOverNote = note; + repaintNote (mouseOverNote); + } +} + +} // namespace yup diff --git a/modules/yup_audio_gui/keyboard/yup_MidiKeyboardComponent.h b/modules/yup_audio_gui/keyboard/yup_MidiKeyboardComponent.h new file mode 100644 index 000000000..6d8b179a6 --- /dev/null +++ b/modules/yup_audio_gui/keyboard/yup_MidiKeyboardComponent.h @@ -0,0 +1,220 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + A component that displays a virtual MIDI keyboard. + + This component renders a piano-style keyboard with white and black keys that + responds to mouse interactions and updates a MidiKeyboardState object. It also + monitors the state to visually show which keys are currently pressed. + + The actual drawing is delegated to the ApplicationTheme system. + + @tags{AudioGUI} +*/ +class YUP_API MidiKeyboardComponent + : public Component + , public MidiKeyboardState::Listener +{ +public: + //============================================================================== + /** The different orientations that the keyboard can have. */ + enum Orientation + { + horizontalKeyboard, + verticalKeyboardFacingLeft, + verticalKeyboardFacingRight + }; + + //============================================================================== + /** Creates a MidiKeyboardComponent. + + @param state the MidiKeyboardState object that this keyboard will use to show + which keys are down, and which the user can use to trigger key events + @param orientation whether the keyboard is horizontal or vertical + */ + MidiKeyboardComponent (MidiKeyboardState& state, Orientation orientation); + + /** Destructor. */ + ~MidiKeyboardComponent() override; + + //============================================================================== + /** Changes the velocity used in midi note-on messages that are triggered by clicking + on the component. + + @param velocity the new velocity, in the range 0 to 1.0 + */ + void setVelocity (float velocity); + + /** Returns the current velocity setting. */ + float getVelocity() const noexcept { return velocity; } + + //============================================================================== + /** Changes the midi channel number that will be used for events triggered by clicking + on the component. + + @param midiChannelNumber the midi channel (1 to 16). Events with midi + channel numbers outside this range are ignored + */ + void setMidiChannel (int midiChannelNumber); + + /** Returns the midi channel that the keyboard is using for midi messages. */ + int getMidiChannel() const noexcept { return midiChannel; } + + //============================================================================== + /** Changes the number of octaves displayed by the keyboard. + + @param numOctaves the number of octaves to display + */ + void setOctaveForMiddleC (int octaveNumber); + + /** Returns the number of octaves currently being displayed. */ + int getOctaveForMiddleC() const noexcept { return octaveNumForMiddleC; } + + //============================================================================== + /** Changes the lowest visible key on the keyboard. + + @param noteNumber the midi note number (0-127) of the lowest key to be shown + */ + void setLowestVisibleKey (int noteNumber); + + /** Returns the lowest visible key. */ + int getLowestVisibleKey() const noexcept { return rangeStart; } + + /** Sets the range of keys that the keyboard will display. + + @param lowestNote the lowest key (0-127) + @param highestNote the highest key (0-127) + */ + void setAvailableRange (int lowestNote, int highestNote); + + /** Returns the highest key that is shown on the keyboard. */ + int getHighestVisibleKey() const noexcept { return rangeEnd; } + + Range getKeyStartRange() const; + + //============================================================================== + /** Returns the position within the component of a key. + + @param midiNoteNumber the note to find the position of + @returns the key's rectangle, or an empty rectangle if the key isn't visible + */ + Rectangle getRectangleForKey (int midiNoteNumber) const; + + /** Returns the note number of the key at a given position within the component. + + @param position the position to search + @returns the midi note number of the key, or -1 if there's no key there + */ + int getNoteAtPosition (Point position) const; + + //============================================================================== + + bool isNoteOn (int midiNoteNumber) const; + + //============================================================================== + + virtual bool isBlackKey (int midiNoteNumber) const; + + int getNumWhiteKeysInRange (int rangeStart, int rangeEnd) const; + + //============================================================================== + /** Color identifiers used by the midi keyboard component. */ + struct Style + { + static const Identifier whiteKeyColorId; + static const Identifier whiteKeyPressedColorId; + static const Identifier whiteKeyShadowColorId; + static const Identifier blackKeyColorId; + static const Identifier blackKeyPressedColorId; + static const Identifier blackKeyShadowColorId; + static const Identifier keyOutlineColorId; + }; + + //============================================================================== + /** @internal */ + void paint (Graphics& g) override; + /** @internal */ + void mouseDown (const MouseEvent& e) override; + /** @internal */ + void mouseDrag (const MouseEvent& e) override; + /** @internal */ + void mouseUp (const MouseEvent& e) override; + /** @internal */ + void mouseMove (const MouseEvent& e) override; + /** @internal */ + void mouseEnter (const MouseEvent& e) override; + /** @internal */ + void mouseExit (const MouseEvent& e) override; + /** @internal */ + void mouseWheel (const MouseEvent& e, const MouseWheelData& wheel) override; + /** @internal */ + void handleNoteOn (MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity) override; + /** @internal */ + void handleNoteOff (MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity) override; + /** @internal */ + void resized() override; + /** @internal */ + void keyDown (const KeyPress& key, const Point& position) override; + /** @internal */ + void focusLost () override; + + //============================================================================== + /** @internal */ + void getKeyPosition (int midiNoteNumber, float keyWidth, Rectangle& keyPos, bool& isBlack) const; + /** @internal */ + bool isMouseOverNote (int midiNoteNumber) const { return midiNoteNumber == mouseOverNote; } + +private: + //============================================================================== + MidiKeyboardState& state; + + int midiChannel = 1; + int midiInChannelMask = 0xffff; + float velocity = 1.0f; + + int rangeStart = 12; + int rangeEnd = 96; + int octaveNumForMiddleC = 3; + + Orientation orientation; + + Array mouseDownNotes; + int mouseOverNote = -1; + bool shouldCheckState = false; + + String getWhiteNoteText (int midiNoteNumber); + int xyToNote (Point pos, float& mousePositionVelocity); + int remappedXYToNote (Point pos, float& mousePositionVelocity) const; + void repaintNote (int midiNoteNumber); + void updateNoteUnderMouse (Point pos, bool isDown, int fingerNum); + void updateNoteUnderMouse (const MouseEvent& e, bool isDown); + void resetAnyKeysInUse(); + void updateShadowNoteUnderMouse (const MouseEvent& e); + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiKeyboardComponent) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/yup_audio_gui.cpp b/modules/yup_audio_gui/yup_audio_gui.cpp new file mode 100644 index 000000000..9d908d2d9 --- /dev/null +++ b/modules/yup_audio_gui/yup_audio_gui.cpp @@ -0,0 +1,35 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifdef YUP_AUDIO_GUI_H_INCLUDED + /* When you add this cpp file to your project, you mustn't include it in a file where you've + already included any other headers - just put it inside a file on its own, possibly with your config + flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix + header files that the compiler may be using. + */ + #error "Incorrect use of YUP cpp file" +#endif + +#include "yup_audio_gui.h" + +//============================================================================== + +#include "keyboard/yup_MidiKeyboardComponent.cpp" diff --git a/modules/yup_audio_gui/yup_audio_gui.h b/modules/yup_audio_gui/yup_audio_gui.h new file mode 100644 index 000000000..cd3f0f2d4 --- /dev/null +++ b/modules/yup_audio_gui/yup_audio_gui.h @@ -0,0 +1,50 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +/* + ============================================================================== + + BEGIN_YUP_MODULE_DECLARATION + + ID: yup_audio_gui + vendor: yup + version: 1.0.0 + name: YUP Audio GUI Components + description: Audio-related GUI components for the YUP library + website: https://github.com/kunitoki/yup + license: ISC + + dependencies: yup_audio_basics yup_gui + + END_YUP_MODULE_DECLARATION + + ============================================================================== +*/ + +#pragma once +#define YUP_AUDIO_GUI_H_INCLUDED + +#include +#include + +//============================================================================== + +#include "keyboard/yup_MidiKeyboardComponent.h" \ No newline at end of file diff --git a/modules/yup_audio_gui/yup_audio_gui.mm b/modules/yup_audio_gui/yup_audio_gui.mm new file mode 100644 index 000000000..b4c3aa977 --- /dev/null +++ b/modules/yup_audio_gui/yup_audio_gui.mm @@ -0,0 +1,22 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "yup_audio_gui.cpp" diff --git a/modules/yup_graphics/fonts/yup_Font.cpp b/modules/yup_graphics/fonts/yup_Font.cpp index cea80740a..8590cc19c 100644 --- a/modules/yup_graphics/fonts/yup_Font.cpp +++ b/modules/yup_graphics/fonts/yup_Font.cpp @@ -55,6 +55,12 @@ Font::Font (rive::rcp font) { } +Font::Font (rive::rcp font, float height) + : font (std::move (font)) + , height (height) +{ +} + //============================================================================== Result Font::loadFromData (const MemoryBlock& fontBytes) @@ -114,6 +120,25 @@ bool Font::isItalic() const //============================================================================== +float Font::getHeight() const noexcept +{ + return height; +} + +void Font::setHeight (float newHeight) +{ + height = newHeight; +} + +Font Font::withHeight (float height) const +{ + Font result (*this); + result.setHeight (height); + return result; +} + +//============================================================================== + int Font::getNumAxis() const { return font != nullptr ? static_cast (font->getAxisCount()) : 0; diff --git a/modules/yup_graphics/fonts/yup_Font.h b/modules/yup_graphics/fonts/yup_Font.h index b726db401..03222a508 100644 --- a/modules/yup_graphics/fonts/yup_Font.h +++ b/modules/yup_graphics/fonts/yup_Font.h @@ -70,6 +70,14 @@ class YUP_API Font /** Returns true if the font is italic. */ bool isItalic() const; + //============================================================================== + + float getHeight() const noexcept; + + void setHeight (float newHeight); + + Font withHeight (float height) const; + //============================================================================== /** Axis. @@ -244,10 +252,13 @@ class YUP_API Font /** @internal */ Font (rive::rcp font); /** @internal */ + Font (rive::rcp font, float height); + /** @internal */ rive::rcp getFont() const; private: rive::rcp font; + float height = 12.0f; }; } // namespace yup diff --git a/modules/yup_graphics/fonts/yup_StyledText.cpp b/modules/yup_graphics/fonts/yup_StyledText.cpp index b12f00908..82279e60e 100644 --- a/modules/yup_graphics/fonts/yup_StyledText.cpp +++ b/modules/yup_graphics/fonts/yup_StyledText.cpp @@ -73,21 +73,19 @@ void StyledText::TextModifier::clear() void StyledText::TextModifier::appendText (StringRef text, const Font& font, - float fontSize, float lineHeight, float letterSpacing) { - styledText.appendText (text, nullptr, font, fontSize, lineHeight, letterSpacing); + styledText.appendText (text, nullptr, font, lineHeight, letterSpacing); } void StyledText::TextModifier::appendText (StringRef text, rive::rcp paint, const Font& font, - float fontSize, float lineHeight, float letterSpacing) { - styledText.appendText (text, paint, font, fontSize, lineHeight, letterSpacing); + styledText.appendText (text, paint, font, lineHeight, letterSpacing); } void StyledText::TextModifier::setOverflow (StyledText::TextOverflow value) @@ -256,7 +254,6 @@ void StyledText::setWrap (TextWrap value) void StyledText::appendText (StringRef text, rive::rcp paint, const Font& font, - float fontSize, float lineHeight, float letterSpacing) { @@ -276,6 +273,7 @@ void StyledText::appendText (StringRef text, styles.emplace_back (paint, std::move (path), true); } + float fontSize = font.getHeight(); styledTexts.append (font.getFont(), fontSize, lineHeight, letterSpacing, (const char*) text, styleIndex); isDirty = true; diff --git a/modules/yup_graphics/fonts/yup_StyledText.h b/modules/yup_graphics/fonts/yup_StyledText.h index eefd932b6..a72c652ce 100644 --- a/modules/yup_graphics/fonts/yup_StyledText.h +++ b/modules/yup_graphics/fonts/yup_StyledText.h @@ -83,14 +83,12 @@ class YUP_API StyledText void appendText (StringRef text, const Font& font, - float fontSize = 16.0f, float lineHeight = -1.0f, float letterSpacing = 0.0f); void appendText (StringRef text, rive::rcp paint, const Font& font, - float fontSize = 16.0f, float lineHeight = -1.0f, float letterSpacing = 0.0f); @@ -140,10 +138,7 @@ class YUP_API StyledText struct RenderStyle { - RenderStyle ( - rive::rcp paint, - rive::rcp path, - bool isEmpty) + RenderStyle (rive::rcp paint, rive::rcp path, bool isEmpty) : paint (std::move (paint)) , path (std::move (path)) , isEmpty (isEmpty) @@ -198,7 +193,6 @@ class YUP_API StyledText void appendText (StringRef text, rive::rcp paint, const Font& font, - float fontSize, float lineHeight, float letterSpacing); diff --git a/modules/yup_graphics/graphics/yup_Color.h b/modules/yup_graphics/graphics/yup_Color.h index f8af8084b..943abdc4e 100644 --- a/modules/yup_graphics/graphics/yup_Color.h +++ b/modules/yup_graphics/graphics/yup_Color.h @@ -777,6 +777,25 @@ class YUP_API Color return result; } + //============================================================================== + constexpr Color overlaidWith (Color src) const noexcept + { + auto destAlpha = getAlpha(); + if (destAlpha <= 0) + return src; + + auto invA = 0xff - static_cast (src.getAlpha()); + auto resA = 0xff - (((0xff - destAlpha) * invA) >> 8); + if (resA <= 0) + return *this; + + auto da = (invA * destAlpha) / resA; + return Color ((uint8) resA, + (uint8) (src.getRed() + ((((int) getRed() - src.getRed()) * da) >> 8)), + (uint8) (src.getGreen() + ((((int) getGreen() - src.getGreen()) * da) >> 8)), + (uint8) (src.getBlue() + ((((int) getBlue() - src.getBlue()) * da) >> 8))); + } + //============================================================================== // TODO - doxygen static Color opaqueRandom() noexcept diff --git a/modules/yup_graphics/graphics/yup_ColorGradient.h b/modules/yup_graphics/graphics/yup_ColorGradient.h index fff3a61d9..7f9e553bf 100644 --- a/modules/yup_graphics/graphics/yup_ColorGradient.h +++ b/modules/yup_graphics/graphics/yup_ColorGradient.h @@ -47,7 +47,7 @@ class YUP_API ColorGradient /** Constructs a default color stop with zero values. */ constexpr ColorStop() = default; - /** Constructs a color stop with the given color, x, y, and delta. */ + /** Constructs a color stop with the given color, x, y and delta. */ constexpr ColorStop (Color color, float x, float y, float delta) : color (color) , x (x) @@ -56,6 +56,15 @@ class YUP_API ColorGradient { } + /** Constructs a color stop with the given color, point and delta. */ + constexpr ColorStop (Color color, const Point& p, float delta) + : color (color) + , x (p.getX()) + , y (p.getY()) + , delta (delta) + { + } + constexpr ColorStop (const ColorStop& other) noexcept = default; constexpr ColorStop (ColorStop&& other) noexcept = default; constexpr ColorStop& operator= (const ColorStop& other) noexcept = default; @@ -94,6 +103,11 @@ class YUP_API ColorGradient radius = std::sqrt (square (x2 - x1) + square (y2 - y1)); } + ColorGradient (Color color1, const Point& p1, Color color2, const Point& p2, Type type) noexcept + : ColorGradient (color1, p1.getX(), p1.getY(), color2, p2.getX(), p2.getY(), type) + { + } + /** Constructs a gradient with multiple color stops. @param type The type of gradient (Linear or Radial). @@ -248,6 +262,11 @@ class YUP_API ColorGradient }); } + void addColorStop (Color color, const Point& p, float delta) + { + addColorStop (color, p.getX(), p.getY(), delta); + } + /** Clears all color stops. */ void clearStops() { diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index 01ca3125c..3ec4aba40 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -170,6 +170,31 @@ rive::rcp toColorGradient (rive::Factory& factory, const Col } } +//============================================================================== +StyledText::HorizontalAlign toHorizontalAlign (Justification justification) +{ + if (justification.testFlags (Justification::left)) + return StyledText::left; + else if (justification.testFlags (Justification::right)) + return StyledText::right; + else if (justification.testFlags (Justification::horizontalCenter)) + return StyledText::center; + else + return StyledText::center; +} + +StyledText::VerticalAlign toVerticalAlign (Justification justification) +{ + if (justification.testFlags (Justification::top)) + return StyledText::top; + else if (justification.testFlags (Justification::bottom)) + return StyledText::bottom; + else if (justification.testFlags (Justification::verticalCenter)) + return StyledText::middle; + else + return StyledText::middle; +} + } // namespace //============================================================================== @@ -679,6 +704,20 @@ void Graphics::fillFittedText (const StyledText& text, const Rectangle& r renderFittedText (text, rect, std::addressof (paint)); } +void Graphics::fillFittedText (const String& text, const Font& font, const Rectangle& rect, Justification justification) +{ + StyledText styledText; + { + auto modifier = styledText.startUpdate(); + modifier.setMaxSize (rect.getSize()); + modifier.appendText (text, font); + modifier.setHorizontalAlign (toHorizontalAlign (justification)); + modifier.setVerticalAlign (toVerticalAlign (justification)); + } + + fillFittedText (styledText, rect); +} + void Graphics::strokeFittedText (const StyledText& text, const Rectangle& rect) { jassert (! text.needsUpdate()); @@ -702,6 +741,20 @@ void Graphics::strokeFittedText (const StyledText& text, const Rectangle& renderFittedText (text, rect, std::addressof (paint)); } +void Graphics::strokeFittedText (const String& text, const Font& font, const Rectangle& rect, Justification justification) +{ + StyledText styledText; + { + auto modifier = styledText.startUpdate(); + modifier.setMaxSize (rect.getSize()); + modifier.appendText (text, font); + modifier.setHorizontalAlign (toHorizontalAlign (justification)); + modifier.setVerticalAlign (toVerticalAlign (justification)); + } + + strokeFittedText (styledText, rect); +} + void Graphics::renderFittedText (const StyledText& text, const Rectangle& rect, rive::RiveRenderPaint* paint) { jassert (! text.needsUpdate()); diff --git a/modules/yup_graphics/graphics/yup_Graphics.h b/modules/yup_graphics/graphics/yup_Graphics.h index 060dedbab..ceb098ab4 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.h +++ b/modules/yup_graphics/graphics/yup_Graphics.h @@ -452,6 +452,16 @@ class YUP_API Graphics */ void fillFittedText (const StyledText& text, const Rectangle& rect); + /** Fills a text with a specified font and size. + + @param text The text to fill. + @param rect The rectangle that defines the text. + @param font The font to use. + @param fontSize The size of the font. + @param justification The justification of the text. + */ + void fillFittedText (const String& text, const Font& font, const Rectangle& rect, Justification justification = Justification::center); + /** Draws an attributed text. @param text The text to draw. @@ -459,6 +469,16 @@ class YUP_API Graphics */ void strokeFittedText (const StyledText& text, const Rectangle& rect); + /** Draws a text with a specified font and size. + + @param text The text to draw. + @param rect The rectangle that defines the text. + @param font The font to use. + @param fontSize The size of the font. + @param justification The justification of the text. + */ + void strokeFittedText (const String& text, const Font& font, const Rectangle& rect, Justification justification = Justification::center); + //============================================================================== /** Retrieves the global context scale, the one used to construct the graphics instance. */ float getContextScale() const; diff --git a/modules/yup_gui/artboard/yup_Artboard.cpp b/modules/yup_gui/artboard/yup_Artboard.cpp index 4eadc28c6..cc6c395d6 100644 --- a/modules/yup_gui/artboard/yup_Artboard.cpp +++ b/modules/yup_gui/artboard/yup_Artboard.cpp @@ -27,6 +27,7 @@ namespace yup Artboard::Artboard (StringRef componentID) : Component (componentID) { + setOpaque (true); } Artboard::Artboard (StringRef componentID, std::shared_ptr file) @@ -279,12 +280,7 @@ void Artboard::resized() { auto scaleDpi = getScaleDpi(); auto scaledBounds = getBounds() * scaleDpi; - - auto frameBounds = rive::AABB ( - scaledBounds.getX(), - scaledBounds.getY(), - scaledBounds.getX() + scaledBounds.getWidth(), - scaledBounds.getY() + scaledBounds.getHeight()); + auto frameBounds = scaledBounds.toAABB(); rive::AABB artboardBounds; if (artboard != nullptr) diff --git a/modules/yup_gui/buttons/yup_TextButton.cpp b/modules/yup_gui/buttons/yup_TextButton.cpp index ab3e9d5b5..52abcb1c0 100644 --- a/modules/yup_gui/buttons/yup_TextButton.cpp +++ b/modules/yup_gui/buttons/yup_TextButton.cpp @@ -76,7 +76,7 @@ void TextButton::resized() modifier.clear(); if (buttonText.isNotEmpty()) - modifier.appendText (buttonText, font, getHeight() * 0.35f); + modifier.appendText (buttonText, font.withHeight (getHeight() * 0.35f)); } //============================================================================== diff --git a/modules/yup_gui/buttons/yup_ToggleButton.cpp b/modules/yup_gui/buttons/yup_ToggleButton.cpp index 53cc15ffd..d9e000233 100644 --- a/modules/yup_gui/buttons/yup_ToggleButton.cpp +++ b/modules/yup_gui/buttons/yup_ToggleButton.cpp @@ -91,7 +91,7 @@ void ToggleButton::resized() modifier.setHorizontalAlign (StyledText::center); modifier.setVerticalAlign (StyledText::middle); modifier.clear(); - modifier.appendText (buttonText, font, getHeight() * 0.35f); + modifier.appendText (buttonText, font.withHeight (getHeight() * 0.35f)); } } } diff --git a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp index bb2a1fa1c..cf28c7ecd 100644 --- a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp +++ b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp @@ -19,6 +19,10 @@ ============================================================================== */ +#if YUP_MODULE_AVAILABLE_yup_audio_gui +#include +#endif + namespace yup { @@ -29,24 +33,63 @@ extern const std::size_t RobotoFlexFont_size; //============================================================================== -void paintSlider (Graphics& g, const ApplicationTheme& theme, const Slider& s) +struct SliderColors +{ + Color background; + Color track; + Color thumb; + Color thumbOver; + Color thumbDown; + Color text; +}; + +SliderColors getSliderColors (const ApplicationTheme& theme, const Slider& slider) +{ + SliderColors colors; + colors.background = slider.findColor (Slider::Style::backgroundColorId).value_or (Color (0xff3d3d3d)); + colors.track = slider.findColor (Slider::Style::trackColorId).value_or (Color (0xff636363)); + colors.thumb = slider.findColor (Slider::Style::thumbColorId).value_or (Color (0xff4ebfff)); + colors.thumbOver = slider.findColor (Slider::Style::thumbOverColorId).value_or (colors.thumb.brighter (0.3f)); + colors.thumbDown = slider.findColor (Slider::Style::thumbDownColorId).value_or (colors.thumb.darker (0.2f)); + colors.text = slider.findColor (Slider::Style::textColorId).value_or (Colors::white); + return colors; +} + +void paintRotarySlider (Graphics& g, const ApplicationTheme& theme, const Slider& slider, Rectangle sliderBounds, float rotaryStartAngle, float rotaryEndAngle, float sliderValue, bool isMouseOver, bool isMouseDown) { - auto bounds = s.getLocalBounds().reduced (s.proportionOfWidth (0.1f)); + const auto colors = getSliderColors (theme, slider); + + auto bounds = sliderBounds.reduced (slider.proportionOfWidth (0.1f)); const auto center = bounds.getCenter(); - constexpr auto fromRadians = degreesToRadians (135.0f); - constexpr auto toRadians = fromRadians + degreesToRadians (270.0f); + const auto fromRadians = rotaryStartAngle; + const auto toRadians = rotaryEndAngle; + const auto toCurrentRadians = fromRadians + (toRadians - fromRadians) * sliderValue; Path backgroundPath; - backgroundPath.addEllipse (bounds.reduced (s.proportionOfWidth (0.105f))); + backgroundPath.addEllipse (bounds.reduced (slider.proportionOfWidth (0.105f))); - g.setFillColor (Color (0xff3d3d3d)); // TODO - findColor + g.setFillColor (colors.background); g.fillPath (backgroundPath); - g.setStrokeColor (Color (0xff2b2b2b)); // TODO - findColor - g.setStrokeWidth (s.proportionOfWidth (0.0175f)); + g.setStrokeColor (colors.background.darker (0.3f)); + g.setStrokeWidth (slider.proportionOfWidth (0.0175f)); g.strokePath (backgroundPath); + const auto reducedBounds = bounds.reduced (slider.proportionOfWidth (0.175f)); + const auto pos = center.getPointOnCircumference ( + reducedBounds.getWidth() / 2.0f, + reducedBounds.getHeight() / 2.0f, + toCurrentRadians); + + Path foregroundLine; + foregroundLine.addLine (Line (pos, center).keepOnlyStart (0.25f)); + + g.setStrokeCap (StrokeCap::Round); + g.setStrokeColor (colors.text); + g.setStrokeWidth (slider.proportionOfWidth (0.03f)); + g.strokePath (foregroundLine); + Path backgroundArc; backgroundArc.addCenteredArc (center, bounds.getWidth() / 2.0f, @@ -57,12 +100,10 @@ void paintSlider (Graphics& g, const ApplicationTheme& theme, const Slider& s) true); g.setStrokeCap (StrokeCap::Round); - g.setStrokeColor (Color (0xff636363)); // TODO - findColor - g.setStrokeWidth (s.proportionOfWidth (0.075f)); + g.setStrokeColor (colors.track); + g.setStrokeWidth (slider.proportionOfWidth (0.075f)); g.strokePath (backgroundArc); - const auto toCurrentRadians = fromRadians + degreesToRadians (270.0f) * s.getValueNormalised(); - Path foregroundArc; foregroundArc.addCenteredArc (center, bounds.getWidth() / 2.0f, @@ -72,43 +113,189 @@ void paintSlider (Graphics& g, const ApplicationTheme& theme, const Slider& s) toCurrentRadians, true); + auto thumbColor = slider.isMouseOver() ? colors.thumbOver : colors.thumb; + if (! slider.isEnabled()) + thumbColor = thumbColor.withAlpha (0.3f); + g.setStrokeCap (StrokeCap::Round); - g.setStrokeColor (s.isMouseOver() ? Color (0xff4ebfff).brighter (0.3f) : Color (0xff4ebfff)); // TODO - findColor - g.setStrokeWidth (s.proportionOfWidth (0.075f)); + g.setStrokeColor (thumbColor); + g.setStrokeWidth (slider.proportionOfWidth (0.075f)); g.strokePath (foregroundArc); - const auto reducedBounds = bounds.reduced (s.proportionOfWidth (0.175f)); - const auto pos = center.getPointOnCircumference ( - reducedBounds.getWidth() / 2.0f, - reducedBounds.getHeight() / 2.0f, - toCurrentRadians); + if (slider.hasKeyboardFocus()) + { + Path focusPath; + focusPath.addEllipse (slider.getLocalBounds().reduced (2)); - Path foregroundLine; - foregroundLine.addLine (Line (pos, center).keepOnlyStart (0.25f)); + g.setStrokeColor (Colors::cornflowerblue); // TODO - findColor + g.setStrokeWidth (2.0f); + g.strokePath (focusPath); + } +} - g.setStrokeCap (StrokeCap::Round); - g.setStrokeColor (Color (0xffffffff)); // TODO - findColor - g.setStrokeWidth (s.proportionOfWidth (0.03f)); - g.strokePath (foregroundLine); +void paintLinearSlider (Graphics& g, const ApplicationTheme& theme, const Slider& slider, Rectangle sliderBounds, Rectangle thumbBounds, bool isHorizontal, float sliderValue, bool isMouseOver, bool isMouseDown) +{ + const auto colors = getSliderColors (theme, slider); - /* - const auto& font = theme.getDefaultFont(); - StyledText text; - text.appendText (font, s.proportionOfHeight (0.1f), s.proportionOfHeight (0.1f), String (s.getValue(), 3).toRawUTF8()); - text.layout (s.getLocalBounds().reduced (5).removeFromBottom (s.proportionOfWidth (0.1f)), StyledText::center); + // Draw track background + g.setFillColor (colors.background); + if (isHorizontal) + g.fillRoundedRect (sliderBounds.getX(), sliderBounds.getCenterY() - 2.0f, sliderBounds.getWidth(), 4.0f, 2.0f); + else + g.fillRoundedRect (sliderBounds.getCenterX() - 2.0f, sliderBounds.getY(), 4.0f, sliderBounds.getHeight(), 2.0f); - g.setStrokeColor (Color (0xffffffff)); - g.strokeFittedText (text, s.getLocalBounds().reduced (5).removeFromBottom (s.proportionOfWidth (0.1f))); - */ + // Draw value track for bar sliders + const auto sliderType = slider.getSliderType(); + if (sliderType == Slider::LinearBarHorizontal || sliderType == Slider::LinearBarVertical) + { + g.setFillColor (colors.track); + if (isHorizontal) + g.fillRoundedRect (sliderBounds.getX(), sliderBounds.getCenterY() - 2.0f, sliderValue * sliderBounds.getWidth(), 4.0f, 2.0f); + else + g.fillRoundedRect (sliderBounds.getCenterX() - 2.0f, + sliderBounds.getBottom() - (sliderValue * sliderBounds.getHeight()), + 4.0f, + sliderValue * sliderBounds.getHeight(), + 2.0f); + } + + // Draw thumb + g.setFillColor (isMouseDown ? colors.thumbDown : (isMouseOver ? colors.thumbOver : colors.thumb)); + g.fillEllipse (thumbBounds); - if (s.hasKeyboardFocus()) + // Draw focus outline if needed + if (slider.hasKeyboardFocus()) { - Path focusPath; - focusPath.addEllipse (s.getLocalBounds().reduced (2)); + g.setStrokeColor (Colors::cornflowerblue); + g.setStrokeWidth (2.0f); + g.strokeRoundedRect (slider.getLocalBounds().reduced (2), 2.0f); + } +} - g.setStrokeColor (Colors::cornflowerblue); // TODO - findColor +void paintTwoValueSlider (Graphics& g, const ApplicationTheme& theme, const Slider& slider, Rectangle sliderBounds, Rectangle minThumbBounds, Rectangle maxThumbBounds, bool isHorizontal, float minValue, float maxValue, bool isMouseOverMinThumb, bool isMouseOverMaxThumb, bool isMouseDown) +{ + const auto colors = getSliderColors (theme, slider); + + // Draw track background + g.setFillColor (colors.background); + if (isHorizontal) + g.fillRoundedRect (sliderBounds.getX(), sliderBounds.getCenterY() - 2.0f, sliderBounds.getWidth(), 4.0f, 2.0f); + else + g.fillRoundedRect (sliderBounds.getCenterX() - 2.0f, sliderBounds.getY(), 4.0f, sliderBounds.getHeight(), 2.0f); + + // Draw selected range + g.setFillColor (colors.track); + if (isHorizontal) + { + const float startX = sliderBounds.getX() + (minValue * sliderBounds.getWidth()); + const float endX = sliderBounds.getX() + (maxValue * sliderBounds.getWidth()); + g.fillRoundedRect (startX, sliderBounds.getCenterY() - 2.0f, endX - startX, 4.0f, 2.0f); + } + else + { + const float startY = sliderBounds.getBottom() - (minValue * sliderBounds.getHeight()); + const float endY = sliderBounds.getBottom() - (maxValue * sliderBounds.getHeight()); + g.fillRoundedRect (sliderBounds.getCenterX() - 2.0f, endY, 4.0f, startY - endY, 2.0f); + } + + // Draw min thumb + g.setFillColor (isMouseDown ? colors.thumbDown : (isMouseOverMinThumb ? colors.thumbOver : colors.thumb)); + g.fillEllipse (minThumbBounds); + + // Draw max thumb + g.setFillColor (isMouseDown ? colors.thumbDown : (isMouseOverMaxThumb ? colors.thumbOver : colors.thumb)); + g.fillEllipse (maxThumbBounds); + + // Draw focus outline if needed + if (slider.hasKeyboardFocus()) + { + g.setStrokeColor (Colors::cornflowerblue); g.setStrokeWidth (2.0f); - g.strokePath (focusPath); + g.strokeRoundedRect (slider.getLocalBounds().reduced (2), 2.0f); + } +} + +void paintSlider (Graphics& g, const ApplicationTheme& theme, const Slider& s) +{ + auto sliderBounds = s.getSliderBounds(); + const auto sliderType = s.getSliderType(); + const bool isMouseOver = s.isMouseOver(); + const bool isMouseDown = false; // s.isMouseButtonDown(); + + switch (sliderType) + { + case Slider::RotaryHorizontalDrag: + case Slider::RotaryVerticalDrag: + case Slider::Rotary: + default: + { + constexpr float rotaryStartAngle = degreesToRadians (135.0f); + constexpr float rotaryEndAngle = rotaryStartAngle + degreesToRadians (270.0f); + paintRotarySlider (g, theme, s, sliderBounds, rotaryStartAngle, rotaryEndAngle, static_cast (s.getValueNormalised()), isMouseOver, isMouseDown); + break; + } + + case Slider::LinearHorizontal: + case Slider::LinearVertical: + case Slider::LinearBarHorizontal: + case Slider::LinearBarVertical: + { + Rectangle thumbBounds; + const bool isHorizontal = (sliderType == Slider::LinearHorizontal || sliderType == Slider::LinearBarHorizontal); + const float sliderValue = static_cast (s.getValueNormalised()); + + if (isHorizontal) + { + const float thumbX = sliderBounds.getX() + (sliderValue * sliderBounds.getWidth()) - 8.0f; + thumbBounds = Rectangle (thumbX, sliderBounds.getCenterY() - 8.0f, 16.0f, 16.0f); + } + else + { + const float thumbY = sliderBounds.getBottom() - (sliderValue * sliderBounds.getHeight()) - 8.0f; + thumbBounds = Rectangle (sliderBounds.getCenterX() - 8.0f, thumbY, 16.0f, 16.0f); + } + + paintLinearSlider (g, theme, s, sliderBounds, thumbBounds, isHorizontal, sliderValue, isMouseOver, isMouseDown); + break; + } + + case Slider::TwoValueHorizontal: + case Slider::TwoValueVertical: + { + Rectangle minThumbBounds, maxThumbBounds; + const bool isHorizontal = (sliderType == Slider::TwoValueHorizontal); + const float minNorm = 0.0f; // static_cast(s.getMinValueNormalised()); + const float maxNorm = 1.0f; // static_cast(s.getMaxValueNormalised()); + + if (isHorizontal) + { + const float minThumbX = sliderBounds.getX() + (minNorm * sliderBounds.getWidth()) - 8.0f; + const float maxThumbX = sliderBounds.getX() + (maxNorm * sliderBounds.getWidth()) - 8.0f; + minThumbBounds = Rectangle (minThumbX, sliderBounds.getCenterY() - 8.0f, 16.0f, 16.0f); + maxThumbBounds = Rectangle (maxThumbX, sliderBounds.getCenterY() - 8.0f, 16.0f, 16.0f); + } + else + { + const float minThumbY = sliderBounds.getBottom() - (minNorm * sliderBounds.getHeight()) - 8.0f; + const float maxThumbY = sliderBounds.getBottom() - (maxNorm * sliderBounds.getHeight()) - 8.0f; + minThumbBounds = Rectangle (sliderBounds.getCenterX() - 8.0f, minThumbY, 16.0f, 16.0f); + maxThumbBounds = Rectangle (sliderBounds.getCenterX() - 8.0f, maxThumbY, 16.0f, 16.0f); + } + + // For two-value sliders, check which thumb the mouse is over + bool isMouseOverMinThumb = false; + bool isMouseOverMaxThumb = false; + if (isMouseOver) + { + const auto mousePos = Point(); // s.getMousePosition(); + const Point mousePosFloat (static_cast (mousePos.getX()), static_cast (mousePos.getY())); + isMouseOverMinThumb = minThumbBounds.contains (mousePosFloat); + isMouseOverMaxThumb = maxThumbBounds.contains (mousePosFloat); + } + + paintTwoValueSlider (g, theme, s, sliderBounds, minThumbBounds, maxThumbBounds, isHorizontal, minNorm, maxNorm, isMouseOverMinThumb, isMouseOverMaxThumb, isMouseDown); + break; + } } } @@ -360,7 +547,20 @@ void paintLabel (Graphics& g, const ApplicationTheme& theme, const Label& l) auto& styledText = l.getStyledText(); const auto bounds = l.getLocalBounds(); - if (const auto strokeColor = l.findColor (Label::Style::strokeColorId); strokeColor && ! strokeColor->isTransparent()) + if (const auto backgroundColor = l.findColor (Label::Style::backgroundColorId); backgroundColor && ! backgroundColor->isTransparent()) + { + g.setFillColor (*backgroundColor); + g.fillRoundedRect (bounds, 4.0f); + } + + if (const auto outlineColor = l.findColor (Label::Style::outlineColorId); outlineColor && ! outlineColor->isTransparent()) + { + g.setStrokeColor (*outlineColor); + g.setStrokeWidth (2.0f); + g.strokeRoundedRect (bounds, 4.0f); + } + + if (const auto strokeColor = l.findColor (Label::Style::textStrokeColorId); strokeColor && ! strokeColor->isTransparent()) { g.setStrokeColor (*strokeColor); g.setStrokeWidth (l.getStrokeWidth()); @@ -369,7 +569,7 @@ void paintLabel (Graphics& g, const ApplicationTheme& theme, const Label& l) if (! styledText.isEmpty()) { - const auto fillColor = l.findColor (Label::Style::fillColorId).value_or (Colors::white); + const auto fillColor = l.findColor (Label::Style::textFillColorId).value_or (Colors::white); g.setFillColor (fillColor); g.fillFittedText (styledText, bounds); } @@ -482,7 +682,7 @@ void paintPopupMenu (Graphics& g, const ApplicationTheme& theme, const PopupMenu auto styledText = yup::StyledText(); { auto modifier = styledText.startUpdate(); - modifier.appendText (item->text, itemFont, 14.0f); + modifier.appendText (item->text, itemFont.withHeight (14.0f)); } g.fillFittedText (styledText, textRect); @@ -507,7 +707,7 @@ void paintPopupMenu (Graphics& g, const ApplicationTheme& theme, const PopupMenu { auto modifier = styledText.startUpdate(); modifier.setHorizontalAlign (yup::StyledText::right); - modifier.appendText (item->shortcutKeyText, itemFont, 13.0f); + modifier.appendText (item->shortcutKeyText, itemFont.withHeight (13.0f)); } g.setOpacity (0.7f); @@ -567,6 +767,186 @@ void paintPopupMenu (Graphics& g, const ApplicationTheme& theme, const PopupMenu } } +//============================================================================== +#if YUP_MODULE_AVAILABLE_yup_audio_gui +void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKeyboardComponent& keyboard) +{ + auto bounds = keyboard.getLocalBounds(); + + if (bounds.isEmpty()) + return; + + auto keyWidth = keyboard.getKeyStartRange().getLength(); + keyWidth /= keyboard.getNumWhiteKeysInRange (keyboard.getLowestVisibleKey(), keyboard.getHighestVisibleKey() + 1); + + // Draw keyboard background with subtle gradient shadow + auto keyboardWidth = keyboard.getKeyStartRange().getEnd(); + auto shadowColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyShadowColorId); + + if (! shadowColor.isTransparent()) + { + // Draw subtle top shadow gradient for depth + ColorGradient shadowGradient; + shadowGradient.addColorStop (shadowColor, Point (0.0f, 0.0f), 0.0f); + shadowGradient.addColorStop (shadowColor.withAlpha (0.0f), Point (0.0f, 5.0f), 1.0f); + + g.setFillColorGradient (shadowGradient); + g.fillRect (Rectangle (0.0f, 0.0f, keyboardWidth, 5.0f)); + } + + // Draw separator line at bottom + auto lineColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::keyOutlineColorId); + if (! lineColor.isTransparent()) + { + g.setFillColor (lineColor); + g.fillRect (Rectangle (0.0f, bounds.getHeight() - 1.0f, keyboardWidth, 1.0f)); + } + + // Paint white keys first + for (int note = keyboard.getLowestVisibleKey(); note <= keyboard.getHighestVisibleKey(); ++note) + { + if (! keyboard.isBlackKey (note)) + { + bool isBlack; + Rectangle keyArea; + keyboard.getKeyPosition (note, keyWidth, keyArea, isBlack); + + auto isPressed = keyboard.isNoteOn (note); + auto isOver = keyboard.isMouseOverNote (note); + + // Base colors from theme + auto whiteKeyColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyColorId); + auto pressedColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyPressedColorId); + auto outlineColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::keyOutlineColorId); + + // Determine fill color based on state + Color fillColor = whiteKeyColor; + if (isPressed) + fillColor = pressedColor; + if (isOver && ! isPressed) + fillColor = whiteKeyColor.overlaidWith (pressedColor.withAlpha (0.3f)); + + // Fill the key + g.setFillColor (fillColor); + g.fillRect (keyArea); + + // Draw key separator line on the left edge + if (! outlineColor.isTransparent()) + { + g.setFillColor (outlineColor); + g.fillRect (keyArea.removeFromLeft (1.0f)); + + // Draw right edge for the last key + if (note == keyboard.getHighestVisibleKey()) + g.fillRect (keyArea.removeFromRight (1.0f).translated (keyArea.getWidth(), 0.0f)); + } + + // Draw note text if there's space + if (keyboard.getWidth() > 100 && keyArea.getWidth() > 15.0f) + { + auto noteText = String(); + int noteInOctave = note % 12; + switch (noteInOctave) + { + case 0: + noteText = "C"; + break; + case 2: + noteText = "D"; + break; + case 4: + noteText = "E"; + break; + case 5: + noteText = "F"; + break; + case 7: + noteText = "G"; + break; + case 9: + noteText = "A"; + break; + case 11: + noteText = "B"; + break; + default: + break; + } + + if (noteText.isNotEmpty()) + { + auto textColor = outlineColor.contrasting (0.8f); + if (isPressed) + textColor = pressedColor.contrasting (0.8f); + + g.setFillColor (textColor); + + StyledText styledText; + { + auto modifier = styledText.startUpdate(); + modifier.appendText (noteText, theme.getDefaultFont().withHeight (11.0f)); + modifier.setHorizontalAlign (StyledText::center); + } + + auto textArea = keyArea.reduced (2.0f).removeFromBottom (16.0f); + g.fillFittedText (styledText, textArea); + } + } + } + } + + // Paint black keys on top + for (int note = keyboard.getLowestVisibleKey(); note <= keyboard.getHighestVisibleKey(); ++note) + { + if (keyboard.isBlackKey (note)) + { + bool isBlack; + Rectangle keyArea; + keyboard.getKeyPosition (note, keyWidth, keyArea, isBlack); + + auto isPressed = keyboard.isNoteOn (note); + auto isOver = keyboard.isMouseOverNote (note); + + // Base colors from theme + auto blackKeyColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::blackKeyColorId); + auto blackPressedColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::blackKeyPressedColorId); + + // Determine fill color based on state + Color fillColor = blackKeyColor; + if (isPressed) + fillColor = blackPressedColor; + if (isOver && ! isPressed) + fillColor = blackKeyColor.overlaidWith (blackPressedColor.withAlpha (0.3f)); + + // Fill the key + g.setFillColor (fillColor); + g.fillRect (keyArea); + + if (isPressed) + { + // Draw pressed outline + g.setStrokeColor (blackKeyColor); + g.setStrokeWidth (1.0f); + g.strokeRect (keyArea); + } + else + { + // Draw 3D highlight effect for unpressed keys + auto highlightColor = fillColor.brighter (0.4f); + g.setFillColor (highlightColor); + + // Create highlight area - top portion and side edges + auto sideIndent = keyArea.getWidth() * 0.125f; + auto topIndent = keyArea.getHeight() * 0.875f; + auto highlightArea = keyArea.reduced (sideIndent, 0).removeFromTop (topIndent); + + g.fillRect (highlightArea); + } + } + } +} +#endif + //============================================================================== ApplicationTheme::Ptr createThemeVersion1() @@ -582,6 +962,13 @@ ApplicationTheme::Ptr createThemeVersion1() } theme->setComponentStyle (ComponentStyle::createStyle (paintSlider)); + theme->setColor (Slider::Style::backgroundColorId, Color (0xff3d3d3d)); + theme->setColor (Slider::Style::trackColorId, Color (0xff636363)); + theme->setColor (Slider::Style::thumbColorId, Color (0xff4ebfff)); + theme->setColor (Slider::Style::thumbOverColorId, Color (0xff4ebfff).brighter (0.3f)); + theme->setColor (Slider::Style::thumbDownColorId, Color (0xff4ebfff).darker (0.2f)); + theme->setColor (Slider::Style::textColorId, Colors::white); + theme->setComponentStyle (ComponentStyle::createStyle (paintTextButton)); theme->setComponentStyle (ComponentStyle::createStyle (paintToggleButton)); theme->setComponentStyle (ComponentStyle::createStyle (paintSwitchButton)); @@ -589,11 +976,24 @@ ApplicationTheme::Ptr createThemeVersion1() theme->setComponentStyle (ComponentStyle::createStyle (paintComboBox)); theme->setComponentStyle