commit c1cae829183a819603d0898883e5a9cdf4000970 Author: Andrew Belt Date: Mon Nov 14 18:34:40 2016 -0500 Initial commit diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..65c5ca88 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ddcd80a0 --- /dev/null +++ b/Makefile @@ -0,0 +1,78 @@ +ARCH ?= linux +CFLAGS = -MMD -g -Wall -O0 \ + -DNOC_FILE_DIALOG_IMPLEMENTATION +CXXFLAGS = -MMD -g -Wall -std=c++11 -O0 -ffast-math \ + -I./lib -I./src +LDFLAGS = + +SOURCES = $(wildcard src/*.cpp src/*/*.cpp) \ + lib/nanovg/src/nanovg.c + + +# Linux +ifeq ($(ARCH), linux) +CC = gcc +CXX = g++ +SOURCES += lib/noc/noc_file_dialog.c +CFLAGS += -DNOC_FILE_DIALOG_GTK $(shell pkg-config --cflags gtk+-2.0) +CXXFLAGS += -DLINUX +LDFLAGS = -lpthread -lGL -lGLEW -lglfw -ldl -lrtaudio -lrtmidi -lprofiler -ljansson \ + $(shell pkg-config --libs gtk+-2.0) +TARGET = 5V +endif + +# Apple +ifeq ($(ARCH), apple) +OSXCROSS = $(HOME)/pkg/osxcross/target/bin +CC = $(OSXCROSS)/x86_64-apple-darwin15-cc +CXX = $(OSXCROSS)/x86_64-apple-darwin15-c++ +SOURCES += lib/noc/noc_file_dialog.m +CFLAGS += -DNOC_FILE_DIALOG_OSX +CXXFLAGS += -DAPPLE -stdlib=libc++ +TARGET = 5V +endif + +# Windows +ifeq ($(ARCH), windows) +CC = x86_64-w64-mingw32-gcc +CXX = x86_64-w64-mingw32-g++ +SOURCES += lib/noc/noc_file_dialog.c +CFLAGS += -DNOC_FILE_DIALOG_WIN32 +CXXFLAGS += -DWINDOWS +LDFLAGS = -lpthread -ljansson \ + ./lib/nanovg/build/libnanovg.a -lglfw3 -lgdi32 -lopengl32 -lglew32 \ + ./lib/rtaudio/librtaudio.a -lksuser -luuid \ + ./lib/rtmidi/librtmidi.a -lwinmm \ + -lcomdlg32 -lole32 \ + -Wl,--export-all-symbols,--out-implib,lib5V.a -mwindows +TARGET = 5V.exe +endif + + +OBJECTS = $(patsubst %, build/%.o, $(SOURCES)) +DEPS = $(patsubst %, build/%.d, $(SOURCES)) + + +# Final targets + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(CXX) -o $@ $^ $(LDFLAGS) + +# Object targets + +-include $(DEPS) + +build/%.c.o: %.c + @mkdir -p $(@D) + $(CC) $(CFLAGS) -c -o $@ $< + +build/%.cpp.o: %.cpp + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) -c -o $@ $< + +# Utilities + +clean: + rm -rf build diff --git a/README.txt b/README.txt new file mode 100644 index 00000000..497b5b2b --- /dev/null +++ b/README.txt @@ -0,0 +1,15 @@ +░█▀▄░█▀█░█▀▀░█░█ +░█▀▄░█▀█░█░░░█▀▄ +░▀░▀░▀░▀░▀▀▀░▀░▀ + +Virtual modular synthesizer engine + +## Building + +The build system is a mess, but I'll add instructions before I announce the software. + +## License + +All source code in this repository is licensed under the LGPLv3. + +LGPL was chosen over GPL to allow plugins to call functions and inherit object classes from this source code without being forced to use the GPL license. You are thus free to develop proprietary plugins which link to and use the API of this software. diff --git a/src/5V.hpp b/src/5V.hpp new file mode 100644 index 00000000..3e1a16f8 --- /dev/null +++ b/src/5V.hpp @@ -0,0 +1,179 @@ +#pragma once + +#include +#include +#include +#include "widgets.hpp" +#include "rack.hpp" + + +extern Scene *gScene; +extern RackWidget *gRackWidget; + +//////////////////// +// Plugin manager +//////////////////// + +struct Model; + +// Subclass this and return a pointer to a new one when init() is called +struct Plugin { + virtual ~Plugin(); + + // A unique identifier for your plugin, e.g. "simple" + std::string slug; + // Human readable name for your plugin, e.g. "Simple Modular" + std::string name; + // A list of the models made available by this plugin + std::list models; +}; + +struct Model { + virtual ~Model() {} + + Plugin *plugin; + // A unique identifier for the model in this plugin, e.g. "vco" + std::string slug; + // Human readable name for your model, e.g. "VCO" + std::string name; + virtual ModuleWidget *createModuleWidget() { return NULL; } +}; + +extern std::list gPlugins; + +void pluginInit(); +void pluginDestroy(); + +//////////////////// +// midi.cpp +//////////////////// + +void midiInit(); +void midiDestroy(); +int midiPortCount(); +std::string midiPortName(int portId); +void midiPortOpen(int portId); +void midiPortClose(); + +//////////////////// +// gui.cpp +//////////////////// + +extern Vec gMousePos; +extern Widget *gHoveredWidget; +extern Widget *gDraggedWidget; + +void guiInit(); +void guiDestroy(); +void guiRun(); +void guiCursorLock(); +void guiCursorUnlock(); + +int loadFont(std::string filename); +int loadImage(std::string filename); +void drawImage(NVGcontext *vg, Vec pos, int imageId); + +//////////////////// +// rack.cpp +//////////////////// + +void rackInit(); +void rackDestroy(); +void rackStart(); +void rackStop(); +// Does not transfer ownership +void rackAddModule(Module *module); +void rackRemoveModule(Module *module); +// Does not transfer ownership +void rackConnectWire(Wire *wire); +void rackDisconnectWire(Wire *wire); +long rackGetFrame(); +void rackRequestFrame(long frame); +void rackSetParamSmooth(Module *module, int paramId, float value); + +//////////////////// +// Implemented by plugin +//////////////////// + +// Called once to initialize and return Plugin. +// Plugin is destructed by the 5V engine when it closes +extern "C" +Plugin *init(); + +//////////////////// +// Optional helpers for plugins +//////////////////// + +inline +Plugin *createPlugin(std::string slug, std::string name) { + Plugin *plugin = new Plugin(); + plugin->slug = slug; + plugin->name = name; + return plugin; +} + +template +Model *createModel(Plugin *plugin, std::string slug, std::string name) { + struct TModel : Model { + ModuleWidget *createModuleWidget() { + ModuleWidget *moduleWidget = new TModuleWidget(); + moduleWidget->model = this; + return moduleWidget; + } + }; + Model *model = new TModel(); + model->plugin = plugin; + model->slug = slug; + model->name = name; + // Create bi-directional association between the Plugin and Model + if (plugin) { + plugin->models.push_back(model); + } + return model; +} + +template +ParamWidget *createParamWidget(ModuleWidget *moduleWidget, int paramId, float minValue, float maxValue, float defaultValue, Vec pos) { + ParamWidget *param = new TParam(); + param->moduleWidget = moduleWidget; + param->paramId = paramId; + param->setLimits(minValue, maxValue); + param->setDefaultValue(defaultValue); + param->box.pos = pos; + // Create bi-directional association between the Param and ModelWidget + moduleWidget->params[paramId] = param; + moduleWidget->addChild(param); + return param; +} + +inline +InputPort *createInputPort(ModuleWidget *moduleWidget, int inputId, Vec pos) { + InputPort *port = new InputPort(); + port->moduleWidget = moduleWidget; + port->inputId = inputId; + port->box.pos = pos; + // Create bi-directional association between the InputPort and ModelWidget + moduleWidget->inputs[inputId] = port; + moduleWidget->addChild(port); + return port; +} + +inline +OutputPort *createOutputPort(ModuleWidget *moduleWidget, int outputId, Vec pos) { + OutputPort *port = new OutputPort(); + port->moduleWidget = moduleWidget; + port->outputId = outputId; + port->box.pos = pos; + // Create bi-directional association between the OutputPort and ModelWidget + moduleWidget->outputs[outputId] = port; + moduleWidget->addChild(port); + return port; +} + +inline +Screw *createScrew(ModuleWidget *moduleWidget, Vec pos) { + Screw *screw = new Screw(); + screw->box.pos = pos; + moduleWidget->addChild(screw); + return screw; +} diff --git a/src/core/AudioInterface.cpp b/src/core/AudioInterface.cpp new file mode 100644 index 00000000..e12a6dc1 --- /dev/null +++ b/src/core/AudioInterface.cpp @@ -0,0 +1,203 @@ +#include "core.hpp" +#include +#include +#include +#include + + +#define AUDIO_BUFFER_SIZE 16384 + +struct AudioInterface : Module { + enum ParamIds { + NUM_PARAMS + }; + enum InputIds { + AUDIO1_INPUT, + AUDIO2_INPUT, + NUM_INPUTS + }; + enum OutputIds { + NUM_OUTPUTS + }; + + float audio1Buffer[AUDIO_BUFFER_SIZE] = {}; + float audio2Buffer[AUDIO_BUFFER_SIZE] = {}; + // Current frame for step(), called by the rack thread + long bufferFrame = 0; + // Current frame for processAudio(), called by audio thread + long audioFrame = 0; + long audioFrameNeeded = -1; + RtAudio audio; + // The audio thread should wait on the rack thread until the buffer has enough samples + std::mutex mutex; + std::condition_variable cv; + bool running; + + AudioInterface(); + ~AudioInterface(); + void step(); + + void openDevice(int deviceId); + void closeDevice(); + // Blocks until the buffer has enough samples + void processAudio(float *outputBuffer, int frameCount); +}; + + +AudioInterface::AudioInterface() { + params.resize(NUM_PARAMS); + inputs.resize(NUM_INPUTS); + outputs.resize(NUM_OUTPUTS); +} + +AudioInterface::~AudioInterface() { + closeDevice(); +} + +void AudioInterface::step() { + int i = bufferFrame % AUDIO_BUFFER_SIZE; + audio1Buffer[i] = getf(inputs[AUDIO1_INPUT]); + audio2Buffer[i] = getf(inputs[AUDIO2_INPUT]); + std::unique_lock lock(mutex); + bufferFrame++; + if (bufferFrame == audioFrameNeeded) { + lock.unlock(); + cv.notify_all(); + } +} + +int audioCallback(void *outputBuffer, void *inputBuffer, unsigned int nBufferFrames, double streamTime, RtAudioStreamStatus status, void *userData) { + AudioInterface *that = (AudioInterface*) userData; + that->processAudio((float*) outputBuffer, nBufferFrames); + return 0; +} + +void AudioInterface::openDevice(int deviceId) { + assert(!audio.isStreamOpen()); + if (deviceId < 0) { + deviceId = audio.getDefaultOutputDevice(); + } + + RtAudio::StreamParameters streamParams; + streamParams.deviceId = deviceId; + streamParams.nChannels = 2; + streamParams.firstChannel = 0; + unsigned int sampleRate = SAMPLE_RATE; + unsigned int bufferFrames = 256; + + audioFrame = -1; + running = true; + + try { + audio.openStream(&streamParams, NULL, RTAUDIO_FLOAT32, sampleRate, &bufferFrames, &audioCallback, this); + audio.startStream(); + } + catch (RtAudioError &e) { + printf("Could not open audio stream: %s\n", e.what()); + } +} + +void AudioInterface::closeDevice() { + if (!audio.isStreamOpen()) { + return; + } + { + std::unique_lock lock(mutex); + running = false; + } + cv.notify_all(); + + try { + // Blocks until stream thread exits + audio.stopStream(); + audio.closeStream(); + } + catch (RtAudioError &e) { + printf("Could not close audio stream: %s\n", e.what()); + } +} + +void AudioInterface::processAudio(float *outputBuffer, int frameCount) { + std::unique_lock lock(mutex); + if (audioFrame < 0) { + // This audio thread is new. Reset the frame positions + audioFrame = rackGetFrame(); + bufferFrame = audioFrame; + } + audioFrameNeeded = audioFrame + frameCount; + rackRequestFrame(audioFrameNeeded); + // Wait for needed frames + while (running && bufferFrame < audioFrameNeeded) { + cv.wait(lock); + } + // Copy values from internal buffer to audio buffer, while holding the mutex just in case our audio buffer wraps around + for (int frame = 0; frame < frameCount; frame++) { + int i = audioFrame % AUDIO_BUFFER_SIZE; + outputBuffer[2*frame + 0] = audio1Buffer[i] / 5.0; + outputBuffer[2*frame + 1] = audio2Buffer[i] / 5.0; + audioFrame++; + } +} + + +struct AudioItem : MenuItem { + AudioInterface *audioInterface; + int deviceId; + void onAction() { + audioInterface->closeDevice(); + audioInterface->openDevice(deviceId); + } +}; + +struct AudioChoice : ChoiceButton { + AudioInterface *audioInterface; + void onAction() { + MenuOverlay *overlay = new MenuOverlay(); + Menu *menu = new Menu(); + menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); + + int deviceCount = audioInterface->audio.getDeviceCount(); + if (deviceCount == 0) { + MenuLabel *label = new MenuLabel(); + label->text = "No audio devices"; + menu->pushChild(label); + } + for (int deviceId = 0; deviceId < deviceCount; deviceId++) { + RtAudio::DeviceInfo info = audioInterface->audio.getDeviceInfo(deviceId); + if (!info.probed) + continue; + + char text[100]; + snprintf(text, 100, "%s (%d in, %d out)", info.name.c_str(), info.inputChannels, info.outputChannels); + + AudioItem *audioItem = new AudioItem(); + audioItem->audioInterface = audioInterface; + audioItem->deviceId = deviceId; + audioItem->text = text; + menu->pushChild(audioItem); + } + overlay->addChild(menu); + gScene->addChild(overlay); + } +}; + + +AudioInterfaceWidget::AudioInterfaceWidget() : ModuleWidget(new AudioInterface()) { + box.size = Vec(15*4, 380); + inputs.resize(AudioInterface::NUM_INPUTS); + + createInputPort(this, AudioInterface::AUDIO1_INPUT, Vec(15, 120)); + createInputPort(this, AudioInterface::AUDIO2_INPUT, Vec(15, 170)); + + AudioChoice *audioChoice = new AudioChoice(); + audioChoice->audioInterface = dynamic_cast(module); + audioChoice->text = "Audio Interface"; + audioChoice->box.pos = Vec(0, 0); + audioChoice->box.size.x = box.size.x; + addChild(audioChoice); +} + +void AudioInterfaceWidget::draw(NVGcontext *vg) { + bndBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y); + ModuleWidget::draw(vg); +} diff --git a/src/core/MidiInterface.cpp b/src/core/MidiInterface.cpp new file mode 100644 index 00000000..921826bd --- /dev/null +++ b/src/core/MidiInterface.cpp @@ -0,0 +1,207 @@ +#include "core.hpp" +#include +#include +#include +#include + + +struct MidiInterface : Module { + enum ParamIds { + NUM_PARAMS + }; + enum InputIds { + NUM_INPUTS + }; + enum OutputIds { + GATE_OUTPUT, + PITCH_OUTPUT, + NUM_OUTPUTS + }; + + RtMidiIn midi; + std::list notes; + bool pedal = false; + bool gate = false; + int note = 64; // C4 + int pitchWheel = 64; + + MidiInterface(); + ~MidiInterface(); + void step(); + + void openPort(int portId); + void closePort(); + void pressNote(int note); + void releaseNote(int note); + void processMidi(long msg); +}; + + +void midiCallback(double timeStamp, std::vector *message, void *userData) { + MidiInterface *that = (MidiInterface*) userData; + if (message->size() < 3) + return; + + long msg = (message->at(0)) | (message->at(1) << 8) | (message->at(2) << 16); + that->processMidi(msg); +} + +MidiInterface::MidiInterface() { + params.resize(NUM_PARAMS); + inputs.resize(NUM_INPUTS); + outputs.resize(NUM_OUTPUTS); + midi.setCallback(midiCallback, this); +} + +MidiInterface::~MidiInterface() { + closePort(); +} + +void MidiInterface::step() { + if (outputs[GATE_OUTPUT]) { + *outputs[GATE_OUTPUT] = gate ? 5.0 : 0.0; + } + if (outputs[PITCH_OUTPUT]) { + *outputs[PITCH_OUTPUT] = ((note - 64) + 2.0*(pitchWheel - 64) / 64.0) / 12.0; + } +} + +void MidiInterface::openPort(int portId) { + closePort(); + try { + midi.openPort(portId); + } + catch (RtMidiError &e) { + printf("Could not open midi port: %s\n", e.what()); + } +} + +void MidiInterface::closePort() { + if (!midi.isPortOpen()) + return; + midi.closePort(); +} + +void MidiInterface::pressNote(int note) { + auto it = std::find(notes.begin(), notes.end(), note); + if (it != notes.end()) + notes.erase(it); + notes.push_back(note); + this->gate = true; + this->note = note; +} + +void MidiInterface::releaseNote(int note) { + // Remove the note + auto it = std::find(notes.begin(), notes.end(), note); + if (it != notes.end()) + notes.erase(it); + + if (pedal) { + // Don't release if pedal is held + } + else if (!notes.empty()) { + // Play previous note + auto it2 = notes.end(); + it2--; + this->note = *it2; + } + else { + // No notes are held, turn the gate off + this->gate = false; + } +} + +void MidiInterface::processMidi(long msg) { + int channel = msg & 0xf; + int status = (msg >> 4) & 0xf; + int data1 = (msg >> 8) & 0xff; + int data2 = (msg >> 16) & 0xff; + + if (channel != 0) + return; + printf("channel %d status %d data1 %d data2 %d\n", channel, status, data1, data2); + + switch (status) { + // note off + case 0x8: { + releaseNote(data1); + } break; + case 0x9: // note on + if (data2) { + pressNote(data1); + } + else { + // For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released. + releaseNote(data1); + } + break; + case 0xb: // cc + switch (data1) { + case 0x40: + pedal = (data2 >= 64); + releaseNote(-1); + break; + } + break; + case 0xe: // pitch wheel + this->pitchWheel = data2; + break; + } +} + + +struct MidiItem : MenuItem { + MidiInterface *midiInterface; + int portId; + void onAction() { + midiInterface->closePort(); + midiInterface->openPort(portId); + } +}; + +struct MidiChoice : ChoiceButton { + MidiInterface *midiInterface; + void onAction() { + MenuOverlay *overlay = new MenuOverlay(); + Menu *menu = new Menu(); + menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); + + int portCount = midiInterface->midi.getPortCount(); + if (portCount == 0) { + MenuLabel *label = new MenuLabel(); + label->text = "No MIDI devices"; + menu->pushChild(label); + } + for (int portId = 0; portId < portCount; portId++) { + MidiItem *midiItem = new MidiItem(); + midiItem->midiInterface = midiInterface; + midiItem->portId = portId; + midiItem->text = midiInterface->midi.getPortName(); + menu->pushChild(midiItem); + } + overlay->addChild(menu); + gScene->addChild(overlay); + } +}; + + +MidiInterfaceWidget::MidiInterfaceWidget() : ModuleWidget(new MidiInterface()) { + box.size = Vec(15*4, 380); + outputs.resize(MidiInterface::NUM_OUTPUTS); + + createOutputPort(this, MidiInterface::GATE_OUTPUT, Vec(15, 120)); + createOutputPort(this, MidiInterface::PITCH_OUTPUT, Vec(15, 170)); + + MidiChoice *midiChoice = new MidiChoice(); + midiChoice->midiInterface = dynamic_cast(module); + midiChoice->text = "MIDI Interface"; + midiChoice->box.pos = Vec(0, 0); + midiChoice->box.size.x = box.size.x; + addChild(midiChoice); +} + +void MidiInterfaceWidget::draw(NVGcontext *vg) { + bndBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y); + ModuleWidget::draw(vg); +} diff --git a/src/core/core.cpp b/src/core/core.cpp new file mode 100644 index 00000000..c2ad345e --- /dev/null +++ b/src/core/core.cpp @@ -0,0 +1,9 @@ +#include "core.hpp" + + +Plugin *coreInit() { + Plugin *plugin = createPlugin("Core", "Core"); + createModel(plugin, "AudioInterface", "Audio Interface"); + createModel(plugin, "MidiInterface", "MIDI Interface"); + return plugin; +} diff --git a/src/core/core.hpp b/src/core/core.hpp new file mode 100644 index 00000000..f3b7dbde --- /dev/null +++ b/src/core/core.hpp @@ -0,0 +1,18 @@ +#include "../5V.hpp" + + +Plugin *coreInit(); + +//////////////////// +// module widgets +//////////////////// + +struct AudioInterfaceWidget : ModuleWidget { + AudioInterfaceWidget(); + void draw(NVGcontext *vg); +}; + +struct MidiInterfaceWidget : ModuleWidget { + MidiInterfaceWidget(); + void draw(NVGcontext *vg); +}; diff --git a/src/gui.cpp b/src/gui.cpp new file mode 100644 index 00000000..1180d9d1 --- /dev/null +++ b/src/gui.cpp @@ -0,0 +1,294 @@ +#include "5V.hpp" +#include + +// #define NANOVG_GLEW +#define NANOVG_IMPLEMENTATION +#include "../lib/nanovg/src/nanovg.h" +#define NANOVG_GL2_IMPLEMENTATION +#include "../lib/nanovg/src/nanovg_gl.h" +#define BLENDISH_IMPLEMENTATION +#include "../lib/oui/blendish.h" + + +static GLFWwindow *window; +static NVGcontext *vg = NULL; + +Vec gMousePos; +Widget *gHoveredWidget = NULL; +Widget *gDraggedWidget = NULL; + + +void windowSizeCallback(GLFWwindow* window, int width, int height) { + gScene->box.size = Vec(width, height); + gScene->onResize(); +} + +void mouseButtonCallback(GLFWwindow *window, int button, int action, int mods) { + if (gHoveredWidget) { + // onMouseDown and onMouseUp + if (action == GLFW_PRESS) { + gHoveredWidget->onMouseDown(button); + } + else if (action == GLFW_RELEASE) { + gHoveredWidget->onMouseUp(button); + } + } + + // onDragStart, onDragEnd, and onDragDrop + if (button == GLFW_MOUSE_BUTTON_LEFT) { + if (action == GLFW_PRESS) { + if (gHoveredWidget) { + gDraggedWidget = gHoveredWidget; + gDraggedWidget->onDragStart(); + } + } + else if (action == GLFW_RELEASE) { + if (gDraggedWidget) { + Widget *dropped = gScene->pick(gMousePos); + if (dropped) { + dropped->onDragDrop(gDraggedWidget); + } + gDraggedWidget->onDragEnd(); + gDraggedWidget = NULL; + } + } + } +} + +void cursorPosCallback(GLFWwindow* window, double xpos, double ypos) { + Vec mousePos = Vec(xpos, ypos); + Vec mouseRel = mousePos.minus(gMousePos); + gMousePos = mousePos; + + if (glfwGetInputMode(window, GLFW_CURSOR) != GLFW_CURSOR_DISABLED) { + // TODO Lock gMousePos + } + + // onScroll + int middleButton = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE); + if (middleButton == GLFW_PRESS) { + gScene->scrollWidget->onScroll(mouseRel.neg()); + } + + Widget *hovered = gScene->pick(mousePos); + + if (gDraggedWidget) { + // onDragMove + // Drag slower if Ctrl is held + bool fine = glfwGetKey(window, GLFW_KEY_LEFT_CONTROL ) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS; + float factor = fine ? 0.1 : 1.0; + gDraggedWidget->onDragMove(mouseRel.mult(factor)); + // onDragHover + if (hovered) { + hovered->onDragHover(gDraggedWidget); + } + } + else { + // onMouseEnter and onMouseLeave + if (hovered != gHoveredWidget) { + if (gHoveredWidget) { + gHoveredWidget->onMouseLeave(); + } + if (hovered) { + hovered->onMouseEnter(); + } + } + gHoveredWidget = hovered; + + // onMouseMove + if (hovered) { + hovered->onMouseMove(mouseRel); + } + } +} + +void cursorEnterCallback(GLFWwindow* window, int entered) { + if (!entered) { + if (gHoveredWidget) { + gHoveredWidget->onMouseLeave(); + } + gHoveredWidget = NULL; + } +} + +void scrollCallback(GLFWwindow *window, double x, double y) { + Vec scrollRel = Vec(x, y); + // onScroll + gScene->scrollWidget->onScroll(scrollRel.mult(-95)); +} + +void charCallback(GLFWwindow *window, unsigned int value) { +} + +static int lastWindowX, lastWindowY, lastWindowWidth, lastWindowHeight; + +void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { + if (action == GLFW_PRESS) { + if (key == GLFW_KEY_F11 || key == GLFW_KEY_ESCAPE) { + // Toggle fullscreen + GLFWmonitor *monitor = glfwGetWindowMonitor(window); + if (monitor) { + // Window mode + glfwSetWindowMonitor(window, NULL, lastWindowX, lastWindowY, lastWindowWidth, lastWindowHeight, 0); + } + else { + // Fullscreen + glfwGetWindowPos(window, &lastWindowX, &lastWindowY); + glfwGetWindowSize(window, &lastWindowWidth, &lastWindowHeight); + monitor = glfwGetPrimaryMonitor(); + assert(monitor); + const GLFWvidmode *mode = glfwGetVideoMode(monitor); + glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate); + } + } + } +} + +void renderGui() { + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + // Update and render + glViewport(0, 0, width, height); + glClearColor(1.0, 1.0, 1.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + + nvgBeginFrame(vg, width, height, 1.0); + + gScene->draw(vg); + nvgEndFrame(vg); + glfwSwapBuffers(window); +} + +void guiInit() { + int err; + + // Set up GLFW + err = glfwInit(); + assert(err); + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + window = glfwCreateWindow(1020, 700, "5V", NULL, NULL); + assert(window); + glfwMakeContextCurrent(window); + + glfwSetWindowSizeCallback(window, windowSizeCallback); + glfwSetMouseButtonCallback(window, mouseButtonCallback); + // glfwSetCursorPosCallback(window, cursorPosCallback); + glfwSetCursorEnterCallback(window, cursorEnterCallback); + glfwSetScrollCallback(window, scrollCallback); + glfwSetCharCallback(window, charCallback); + glfwSetKeyCallback(window, keyCallback); + + // Set up GLEW + glewExperimental = GL_TRUE; + err = glewInit(); + assert(err == GLEW_OK); + // GLEW generates GL error because it calls glGetString(GL_EXTENSIONS), we'll consume it here. + glGetError(); + + + // Set up NanoVG + vg = nvgCreateGL2(NVG_ANTIALIAS); + assert(vg); + + // Set up Blendish + bndSetFont(loadFont("res/DejaVuSans.ttf")); + bndSetIconImage(loadImage("res/blender_icons16.png")); +} + +void guiDestroy() { + nvgDeleteGL2(vg); + glfwDestroyWindow(window); + glfwTerminate(); +} + +void guiRun() { + assert(window); + { + int width, height; + glfwGetWindowSize(window, &width, &height); + windowSizeCallback(window, width, height); + } + double lastTime = 0.0; + while(!glfwWindowShouldClose(window)) { + glfwPollEvents(); + { + double xpos, ypos; + glfwGetCursorPos(window, &xpos, &ypos); + cursorPosCallback(window, xpos, ypos); + } + gScene->step(); + renderGui(); + + double currTime = glfwGetTime(); + // printf("%lf fps\n", 1.0/(currTime - lastTime)); + lastTime = currTime; + (void) lastTime; + } +} + +void guiCursorLock() { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); +} + +void guiCursorUnlock() { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); +} + + + +std::map images; +std::map fonts; + +int loadImage(std::string filename) { + assert(vg); + int imageId; + auto it = images.find(filename); + if (it == images.end()) { + // Load image + imageId = nvgCreateImage(vg, filename.c_str(), NVG_IMAGE_REPEATX | NVG_IMAGE_REPEATY); + if (imageId == 0) { + printf("Failed to load image %s\n", filename.c_str()); + } + else { + printf("Loaded image %s\n", filename.c_str()); + } + images[filename] = imageId; + } + else { + imageId = it->second; + } + return imageId; +} + +int loadFont(std::string filename) { + assert(vg); + int fontId; + auto it = fonts.find(filename); + if (it == fonts.end()) { + fontId = nvgCreateFont(vg, filename.c_str(), filename.c_str()); + if (fontId < 0) { + printf("Failed to load font %s\n", filename.c_str()); + } + else { + printf("Loaded font %s\n", filename.c_str()); + } + fonts[filename] = fontId; + } + else { + fontId = it->second; + } + return fontId; +} + + +void drawImage(NVGcontext *vg, Vec pos, int imageId) { + int width, height; + nvgImageSize(vg, imageId, &width, &height); + NVGpaint paint = nvgImagePattern(vg, pos.x, pos.y, width, height, 0, imageId, 1.0); + nvgFillPaint(vg, paint); + nvgRect(vg, pos.x, pos.y, width, height); + nvgFill(vg); +} diff --git a/src/gui.cpp.d b/src/gui.cpp.d new file mode 100644 index 00000000..63282416 --- /dev/null +++ b/src/gui.cpp.d @@ -0,0 +1,4 @@ +../src/gui.cpp.o: ../src/gui.cpp ../src/5V.hpp ../src/widgets.hpp \ + ../src/../lib/nanovg/src/nanovg.h ../src/../lib/oui/blendish.h \ + ../src/rack.hpp ../src/util.hpp ../src/../lib/nanovg/src/nanovg_gl.h \ + ../src/../lib/nanovg/src/nanovg.h diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 00000000..58cbe7f4 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,32 @@ +#include "5V.hpp" +#include + + +Scene *gScene = NULL; +RackWidget *gRackWidget = NULL; + + +int main() { + pluginInit(); + rackInit(); + guiInit(); + gScene = new Scene(); + // audioInit(); + // audioDeviceOpen(); + // midiInit(); + rackStart(); + + // Blocks until user exits + guiRun(); + + // Cleanup + // midiDestroy(); + // audioDeviceClose(); + // audioDestroy(); + delete gScene; + guiDestroy(); + rackStop(); + rackDestroy(); + pluginDestroy(); + return 0; +} diff --git a/src/plugin.cpp b/src/plugin.cpp new file mode 100644 index 00000000..3ea404fc --- /dev/null +++ b/src/plugin.cpp @@ -0,0 +1,98 @@ +#include + +#if defined(WINDOWS) + #include +#elif defined(LINUX) || defined(APPLE) + #include + #include +#endif + +#include "5V.hpp" +#include "core/core.hpp" + + +std::list gPlugins; + +int loadPlugin(const char *path) { + // Load dynamic/shared library + #if defined(WINDOWS) + HINSTANCE handle = LoadLibrary(path); + if (!handle) { + fprintf(stderr, "Failed to load library %s\n", path); + return -1; + } + #elif defined(LINUX) || defined(APPLE) + char ppath[512]; + snprintf(ppath, 512, "./%s", path); + void *handle = dlopen(ppath, RTLD_NOW | RTLD_GLOBAL); + if (!handle) { + fprintf(stderr, "Failed to load library %s: %s\n", path, dlerror()); + return -1; + } + #endif + + // Call plugin init() function + typedef Plugin *(*InitCallback)(); + InitCallback initCallback; + #if defined(WINDOWS) + initCallback = (InitCallback) GetProcAddress(handle, "init"); + #elif defined(LINUX) || defined(APPLE) + initCallback = (InitCallback) dlsym(handle, "init"); + #endif + if (!initCallback) { + fprintf(stderr, "Failed to read init() symbol in %s\n", path); + return -2; + } + + // Add plugin to map + Plugin *plugin = initCallback(); + if (!plugin) { + fprintf(stderr, "Library %s did not return a plugin\n", path); + return -3; + } + gPlugins.push_back(plugin); + fprintf(stderr, "Loaded plugin %s\n", path); + return 0; +} + +void pluginInit() { + // Load core + Plugin *corePlugin = coreInit(); + gPlugins.push_back(corePlugin); + + // Search for plugin libraries + #if defined(WINDOWS) + // WIN32_FIND_DATA ffd; + // HANDLE hFind = FindFirstFile("plugins/*/plugin.dll", &ffd); + // if (hFind != INVALID_HANDLE_VALUE) { + // do { + // loadPlugin(ffd.cFileName); + // } while (FindNextFile(hFind, &ffd)); + // } + // FindClose(hFind); + loadPlugin("plugins/Simple/plugin.dll"); + loadPlugin("plugins/AudibleInstruments/plugin.dll"); + + #elif defined(LINUX) || defined(APPLE) + glob_t result; + glob("plugins/*/plugin.so", GLOB_TILDE, NULL, &result); + for (int i = 0; i < (int) result.gl_pathc; i++) { + loadPlugin(result.gl_pathv[i]); + } + globfree(&result); + #endif +} + +void pluginDestroy() { + for (Plugin *plugin : gPlugins) { + delete plugin; + } + gPlugins.clear(); +} + + +Plugin::~Plugin() { + for (Model *model : models) { + delete model; + } +} diff --git a/src/plugin.cpp.d b/src/plugin.cpp.d new file mode 100644 index 00000000..582e19a7 --- /dev/null +++ b/src/plugin.cpp.d @@ -0,0 +1,3 @@ +../src/plugin.cpp.o: ../src/plugin.cpp ../src/5V.hpp ../src/widgets.hpp \ + ../src/../lib/nanovg/src/nanovg.h ../src/../lib/oui/blendish.h \ + ../src/rack.hpp ../src/util.hpp ../src/core/core.hpp diff --git a/src/rack.cpp b/src/rack.cpp new file mode 100644 index 00000000..925f57ce --- /dev/null +++ b/src/rack.cpp @@ -0,0 +1,180 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "rack.hpp" + + +static std::thread thread; +static std::mutex mutex; +static std::condition_variable cv; +static long frame; +static long frameLimit; +static bool running; + +static std::set modules; +// Merely used for keeping track of which module inputs point to which module outputs, to prevent pointer mistakes and make the rack API rigorous +static std::set wires; + + +static Module *smoothModule = NULL; +static int smoothParamId; +static float smoothValue; + + +void rackInit() { +} + +void rackDestroy() { + // Make sure there are no wires or modules in the rack on destruction. This suggests that a module failed to remove itself when the GUI was destroyed. + assert(wires.empty()); + assert(modules.empty()); +} + +void rackStep() { + // Param interpolation + if (smoothModule) { + float value = smoothModule->params[smoothParamId]; + const float minSpeed = 0.01 * 60.0 / SAMPLE_RATE; // Roughly 0.01 every graphics frame + const float lpCoeff = 60.0 / SAMPLE_RATE / 1.0; // decay rate is 1 graphics frame + float delta = smoothValue - value; + float speed = fmaxf(fabsf(delta) * lpCoeff, minSpeed); + + if (delta < 0) { + value -= speed; + if (value < smoothValue) value = smoothValue; + } + else if (delta > 0) { + value += speed; + if (value > smoothValue) value = smoothValue; + } + + smoothModule->params[smoothParamId] = value; + if (value == smoothValue) { + smoothModule = NULL; + } + } + // Step all modules + for (Module *module : modules) { + module->step(); + } +} + +void rackRun() { + while (1) { + std::unique_lock lock(mutex); + if (!running) + break; + if (frame >= frameLimit) { + // Delay for at most 1ms if there are no needed frames + cv.wait_for(lock, std::chrono::milliseconds(1)); + } + frame++; + lock.unlock(); + // Speed up + // for (int i = 0; i < 16; i++) + rackStep(); + } +} + +void rackStart() { + frame = 0; + frameLimit = 0; + running = true; + thread = std::thread(rackRun); +} + +void rackStop() { + { + std::unique_lock lock(mutex); + running = false; + } + cv.notify_all(); + thread.join(); +} + +void rackAddModule(Module *module) { + assert(module); + // Check that the module is not already added + assert(modules.find(module) == modules.end()); + modules.insert(module); +} + +void rackRemoveModule(Module *module) { + assert(module); + // Remove parameter interpolation which point to this module + if (module == smoothModule) { + smoothModule = NULL; + } + // FIXME use a mutex here + // Check that all wires are disconnected + for (Wire *wire : wires) { + assert(wire->outputModule != module); + assert(wire->inputModule != module); + } + auto it = modules.find(module); + if (it != modules.end()) { + modules.erase(it); + } +} + +void rackConnectWire(Wire *wire) { + assert(wire); + // It would probably be good to reset the wire voltage + wire->value = 0.0; + // Check that the wire is not already added + assert(wires.find(wire) == wires.end()); + assert(wire->outputModule); + assert(wire->inputModule); + // Check that the inputs/outputs are not already used by another cable + for (Wire *wire2 : wires) { + assert(wire2 != wire); + assert(!(wire2->outputModule == wire->outputModule && wire2->outputId == wire->outputId)); + assert(!(wire2->inputModule == wire->inputModule && wire2->inputId == wire->inputId)); + } + // Connect the wire to inputModule + wires.insert(wire); + wire->inputModule->inputs[wire->inputId] = &wire->value; + wire->outputModule->outputs[wire->outputId] = &wire->value; +} + +void rackDisconnectWire(Wire *wire) { + assert(wire); + // Disconnect wire from inputModule + wire->inputModule->inputs[wire->inputId] = NULL; + wire->outputModule->outputs[wire->outputId] = NULL; + + auto it = wires.find(wire); + assert(it != wires.end()); + wires.erase(it); +} + +long rackGetFrame() { + return frame; +} + +void rackRequestFrame(long f) { + std::unique_lock lock(mutex); + if (f > frameLimit) { + frameLimit = f; + lock.unlock(); + cv.notify_all(); + } +} + +void rackSetParamSmooth(Module *module, int paramId, float value) { + // Check existing parameter interpolation + if (smoothModule) { + if (!(smoothModule == module && smoothParamId == paramId)) { + // Jump param value to smooth value + smoothModule->params[smoothParamId] = smoothValue; + } + } + smoothModule = module; + smoothParamId = paramId; + smoothValue = value; +} diff --git a/src/rack.cpp.d b/src/rack.cpp.d new file mode 100644 index 00000000..01b648f5 --- /dev/null +++ b/src/rack.cpp.d @@ -0,0 +1 @@ +../src/rack.cpp.o: ../src/rack.cpp ../src/rack.hpp diff --git a/src/rack.hpp b/src/rack.hpp new file mode 100644 index 00000000..94358706 --- /dev/null +++ b/src/rack.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +// TODO Find a clean way to make this a variable +#define SAMPLE_RATE 44100 + + +struct Wire; + +struct Module { + std::vector params; + // Pointers to voltage values at each port + // If value is NULL, the input/output is disconnected + std::vector inputs; + std::vector outputs; + + virtual ~Module() {} + + // Always called on each sample frame before calling getOutput() + virtual void step() {} +}; + + +struct Wire { + Module *outputModule = NULL; + int outputId; + Module *inputModule = NULL; + int inputId; + // The voltage which is pointed to by module inputs/outputs + float value = 0.0; +}; diff --git a/src/util.hpp b/src/util.hpp new file mode 100644 index 00000000..521fda08 --- /dev/null +++ b/src/util.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include + +//////////////////// +// Utilities +// A header-only file with handy inline functions +//////////////////// + +#ifndef M_PI + #define M_PI 3.141592653589793238462643383 + #define M_E 2.718281828459045235360287471 +#endif + +inline float clampf(float x, float min, float max) { + return fmaxf(min, fminf(max, x)); +} + +inline float mapf(float x, float xMin, float xMax, float yMin, float yMax) { + return yMin + (x - xMin) / (xMax - xMin) * (yMax - yMin); +} + +inline float crossf(float a, float b, float frac) { + return (1 - frac) * a + frac * b; +} + +inline int mini(int a, int b) { + return a < b ? a : b; +} + +inline int maxi(int a, int b) { + return a > b ? a : b; +} + +// Euclidean modulus, always returns 0 <= mod < base for positive base +// Assumes this architecture's division is non-Euclidean +inline int eucMod(int a, int base) { + int mod = a % base; + return mod < 0 ? mod + base : mod; +} + +inline float getf(float *p, float v=0.0) { + return p ? *p : v; +} + +inline void setf(float *p, float v) { + if (p) *p = v; +} + + +struct Vec { + float x, y; + + Vec() : x(0.0), y(0.0) {} + Vec(float x, float y) : x(x), y(y) {} + + Vec neg() { + return Vec(-x, -y); + } + Vec plus(Vec b) { + return Vec(x + b.x, y + b.y); + } + Vec minus(Vec b) { + return Vec(x - b.x, y - b.y); + } + Vec mult(float s) { + return Vec(x * s, y * s); + } + Vec div(float s) { + return Vec(x / s, y / s); + } + float dot(Vec b) { + return x * b.x + y * b.y; + } + float norm() { + return hypotf(x, y); + } + Vec min(Vec b) { + return Vec(fminf(x, b.x), fminf(y, b.y)); + } + Vec max(Vec b) { + return Vec(fmaxf(x, b.x), fmaxf(y, b.y)); + } + Vec round() { + return Vec(roundf(x), roundf(y)); + } +}; + + +struct Rect { + Vec pos; + Vec size; + + Rect() {} + Rect(Vec pos, Vec size) : pos(pos), size(size) {} + + bool contains(Vec v) { + return pos.x <= v.x && v.x < pos.x + size.x + && pos.y <= v.y && v.y < pos.y + size.y; + } + bool intersects(Rect r) { + return (pos.x + size.x > r.pos.x && r.pos.x + r.size.x > pos.x) + && (pos.y + size.y > r.pos.y && r.pos.y + r.size.y > pos.y); + } + Vec getCenter() { + return pos.plus(size.mult(0.5)); + } + Vec getTopRight() { + return pos.plus(Vec(size.x, 0.0)); + } + Vec getBottomLeft() { + return pos.plus(Vec(0.0, size.y)); + } + Vec getBottomRight() { + return pos.plus(size); + } +}; diff --git a/src/widgets.hpp b/src/widgets.hpp new file mode 100644 index 00000000..73b64a5d --- /dev/null +++ b/src/widgets.hpp @@ -0,0 +1,398 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../lib/nanovg/src/nanovg.h" +#include "../lib/oui/blendish.h" + +#include "rack.hpp" +#include "util.hpp" + + +struct MenuEntry; +struct RackWidget; +struct ParamWidget; +struct InputPort; +struct OutputPort; + +//////////////////// +// base class and traits +//////////////////// + +// A node in the 2D scene graph +struct Widget { + // Stores position and size + Rect box = Rect(Vec(0, 0), Vec(INFINITY, INFINITY)); + Widget *parent = NULL; + std::list children; + + virtual ~Widget(); + + Vec getAbsolutePos(); + Rect getChildrenBoundingBox(); + + // Gives ownership of widget to this widget instance + void addChild(Widget *widget); + // Does not delete widget but transfers ownership to caller + // Silenty fails if widget is not a child + void removeChild(Widget *widget); + void clearChildren(); + + // Advances the module by one frame + virtual void step(); + // Draws to NanoVG context + virtual void draw(NVGcontext *vg); + + // Override this to return NULL if the widget is to be "invisible" to mouse events. + // Override this to return `this` if the widget is to override events of its children. + virtual Widget *pick(Vec pos); + + // Events + + virtual void onMouseDown(int button) {} + virtual void onMouseUp(int button) {} + virtual void onMouseMove(Vec mouseRel) {} + virtual void onMouseEnter() {} + virtual void onMouseLeave() {} + virtual void onScroll(Vec scrollRel) {} + virtual void onDragStart() {} + virtual void onDragDrop(Widget *origin) {} + virtual void onDragHover(Widget *origin) {} + virtual void onDragMove(Vec mouseRel) {} + virtual void onDragEnd() {} + virtual void onResize() {} + virtual void onAction() {} + virtual void onChange() {} +}; + +// Widget that does not respond to events, but allows its children to +struct TranslucentWidget : virtual Widget { + Widget *pick(Vec pos) { + Widget *picked = Widget::pick(pos); + if (picked == this) { + return NULL; + } + return picked; + } +}; + +// Widget that does not respond to events +struct TransparentWidget : virtual Widget { + Widget *pick(Vec pos) { return NULL; } +}; + +struct SpriteWidget : virtual Widget { + Vec spriteOffset; + Vec spriteSize; + std::string spriteFilename; + int index = 0; + void draw(NVGcontext *vg); +}; + +struct QuantityWidget : virtual Widget { + float value = 0.0; + float minValue = 0.0; + float maxValue = 1.0; + float defaultValue = 0.0; + std::string label; + // Include a space character if you want a space after the number, e.g. " Hz" + std::string unit; + + void setValue(float value); + void setLimits(float minValue, float maxValue); + void setDefaultValue(float defaultValue); +}; + +//////////////////// +// gui elements +//////////////////// + +struct Label : TransparentWidget { + std::string text; + void draw(NVGcontext *vg); +}; + +// Deletes itself from parent when clicked +struct MenuOverlay : Widget { + void onMouseDown(int button); +}; + +struct Menu : Widget { + Menu() { + box.size = Vec(0, 0); + } + // Transfers ownership, like addChild() + void pushChild(Widget *child); + void draw(NVGcontext *vg); +}; + +struct MenuEntry : Widget { + std::string text; + MenuEntry() { + box.size = Vec(0, BND_WIDGET_HEIGHT); + } + float computeMinWidth(NVGcontext *vg); +}; + +struct MenuLabel : MenuEntry { + void draw(NVGcontext *vg); +}; + +struct MenuItem : MenuEntry { + BNDwidgetState state = BND_DEFAULT; + + void draw(NVGcontext *vg); + + void onMouseUp(int button); + void onMouseEnter(); + void onMouseLeave() ; +}; + +struct Button : Widget { + std::string text; + BNDwidgetState state = BND_DEFAULT; + + Button(); + void draw(NVGcontext *vg); + void onMouseEnter(); + void onMouseLeave() ; + void onDragDrop(Widget *origin); +}; + +struct ChoiceButton : Button { + void draw(NVGcontext *vg); +}; + +struct Slider : QuantityWidget { + BNDwidgetState state = BND_DEFAULT; + + Slider(); + void draw(NVGcontext *vg); + void onDragStart(); + void onDragMove(Vec mouseRel); + void onDragEnd(); +}; + +struct ScrollBar : Widget { + enum { VERTICAL, HORIZONTAL } orientation; + float containerOffset = 0.0; + float containerSize = 0.0; + BNDwidgetState state = BND_DEFAULT; + + ScrollBar(); + void draw(NVGcontext *vg); + void move(float delta); + void onDragStart(); + void onDragMove(Vec mouseRel); + void onDragEnd(); +}; + +// Handles a container with scrollbars +struct ScrollWidget : Widget { + Widget *container; + ScrollBar *hScrollBar; + ScrollBar *vScrollBar; + + ScrollWidget(); + void draw(NVGcontext *vg); + void onResize(); + void onScroll(Vec scrollRel); +}; + +struct Tooltip : TransparentWidget { + void step(); + void draw(NVGcontext *vg); +}; + +//////////////////// +// module +//////////////////// + +// A 1U module should be 15x380. Thus the width of a module should be a factor of 15. +struct Model; +struct ModuleWidget : Widget { + Model *model = NULL; + // Eventually this should be replaced with a `moduleId` which will be used for inter-process communication between the gui world and the audio world. + Module *module = NULL; + // int moduleId; + + std::vector inputs; + std::vector outputs; + std::vector params; + + ModuleWidget(Module *module); + ~ModuleWidget(); + json_t *toJson(); + void fromJson(json_t *root); + void disconnectPorts(); + void resetParams(); + void cloneParams(ModuleWidget *source); + + void draw(NVGcontext *vg); + + bool requested = false; + Vec requestedPos; + Vec dragPos; + void onDragStart(); + void onDragMove(Vec mouseRel); + void onDragEnd(); + void onMouseDown(int button); +}; + +struct WireWidget : Widget { + OutputPort *outputPort = NULL; + InputPort *inputPort = NULL; + Wire *wire = NULL; + NVGcolor color; + + WireWidget(); + ~WireWidget(); + void updateWire(); + void draw(NVGcontext *vg); + void drawOutputPlug(NVGcontext *vg); + void drawInputPlug(NVGcontext *vg); +}; + +struct RackWidget : Widget { + // Only put ModuleWidgets in here + Widget *moduleContainer; + // Only put WireWidgets in here + Widget *wireContainer; + // An unowned reference to the currently dragged wire + WireWidget *activeWire = NULL; + + RackWidget(); + + void clear(); + json_t *toJson(); + void fromJson(json_t *root); + void repositionModule(ModuleWidget *module); + void step(); + void draw(NVGcontext *vg); + + void onMouseDown(int button); +}; + +//////////////////// +// params +//////////////////// + +struct Light : TransparentWidget, SpriteWidget { + NVGcolor color; + void draw(NVGcontext *vg); +}; + +// If you don't add these to your ModuleWidget, it will fall out of the RackWidget +struct Screw : TransparentWidget, SpriteWidget { + Screw(); +}; + +struct ParamWidget : QuantityWidget { + // Ancestor ModuleWidget, used for accessing the Module + ModuleWidget *moduleWidget; + int paramId; + + json_t *toJson(); + void fromJson(json_t *root); + void onMouseDown(int button); + void onChange(); +}; + +struct Knob : ParamWidget, SpriteWidget { + int minIndex, maxIndex, spriteCount; + void step(); + void onDragStart(); + void onDragMove(Vec mouseRel); + void onDragEnd(); +}; + +struct Switch : ParamWidget, SpriteWidget { +}; + +struct ToggleSwitch : virtual Switch { + void onDragStart() { + index = 1; + } + void onDragEnd() { + index = 0; + } + void onDragDrop(Widget *origin) { + if (origin == this) { + // Cycle through modes + // e.g. a range of [0.0, 3.0] would have modes 0, 1, 2, and 3. + float v = value + 1.0; + setValue(v > maxValue ? minValue : v); + } + } +}; + +struct MomentarySwitch : virtual Switch { + void onDragStart() { + setValue(maxValue); + index = 1; + } + void onDragEnd() { + setValue(minValue); + index = 0; + } +}; + +//////////////////// +// ports +//////////////////// + +struct Port : Widget { + // Ancestor ModuleWidget, used for accessing the Module + ModuleWidget *moduleWidget; + WireWidget *connectedWire = NULL; + + Port(); + ~Port(); + void disconnect(); + + int type; + void draw(NVGcontext *vg); + void drawGlow(NVGcontext *vg); + void onDragEnd(); +}; + +struct InputPort : Port { + int inputId; + + void draw(NVGcontext *vg); + void onDragStart(); + void onDragDrop(Widget *origin); +}; + +struct OutputPort : Port { + int outputId; + + void draw(NVGcontext *vg); + void onDragStart(); + void onDragDrop(Widget *origin); +}; + +//////////////////// +// scene +//////////////////// + +struct Toolbar : Widget { + Slider *wireOpacitySlider; + Toolbar(); + void draw(NVGcontext *vg); +}; + +struct Scene : Widget { + Toolbar *toolbar; + ScrollWidget *scrollWidget; + Scene(); + void onResize(); +}; diff --git a/src/widgets/Button.cpp b/src/widgets/Button.cpp new file mode 100644 index 00000000..9d4cd908 --- /dev/null +++ b/src/widgets/Button.cpp @@ -0,0 +1,24 @@ +#include "../5V.hpp" + + +Button::Button() { + box.size.y = BND_WIDGET_HEIGHT; +} + +void Button::draw(NVGcontext *vg) { + bndToolButton(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str()); +} + +void Button::onMouseEnter() { + state = BND_HOVER; +} + +void Button::onMouseLeave() { + state = BND_DEFAULT; +} + +void Button::onDragDrop(Widget *origin) { + if (origin == this) { + onAction(); + } +} diff --git a/src/widgets/ChoiceButton.cpp b/src/widgets/ChoiceButton.cpp new file mode 100644 index 00000000..9dab2a6f --- /dev/null +++ b/src/widgets/ChoiceButton.cpp @@ -0,0 +1,6 @@ +#include "../5V.hpp" + + +void ChoiceButton::draw(NVGcontext *vg) { + bndChoiceButton(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str()); +} diff --git a/src/widgets/InputPort.cpp b/src/widgets/InputPort.cpp new file mode 100644 index 00000000..d147e305 --- /dev/null +++ b/src/widgets/InputPort.cpp @@ -0,0 +1,34 @@ +#include "../5V.hpp" + + +void InputPort::draw(NVGcontext *vg) { + Port::draw(vg); + if (gRackWidget->activeWire && gRackWidget->activeWire->inputPort) { + Port::drawGlow(vg); + } +} + +void InputPort::onDragStart() { + if (connectedWire) { + // Disconnect wire from this port, but set it as the active wire + connectedWire->inputPort = NULL; + connectedWire->updateWire(); + gRackWidget->activeWire = connectedWire; + connectedWire = NULL; + } + else { + connectedWire = new WireWidget(); + connectedWire->inputPort = this; + gRackWidget->wireContainer->addChild(connectedWire); + gRackWidget->activeWire = connectedWire; + } +} + +void InputPort::onDragDrop(Widget *origin) { + if (connectedWire) return; + if (gRackWidget->activeWire) { + if (gRackWidget->activeWire->inputPort) return; + gRackWidget->activeWire->inputPort = this; + connectedWire = gRackWidget->activeWire; + } +} diff --git a/src/widgets/Knob.cpp b/src/widgets/Knob.cpp new file mode 100644 index 00000000..f0a36cef --- /dev/null +++ b/src/widgets/Knob.cpp @@ -0,0 +1,21 @@ +#include "../5V.hpp" + + +#define KNOB_SENSITIVITY 0.001 + + +void Knob::step() { + index = eucMod((int) roundf(mapf(value, minValue, maxValue, minIndex, maxIndex)), spriteCount); +} + +void Knob::onDragStart() { + guiCursorLock(); +} + +void Knob::onDragMove(Vec mouseRel) { + setValue(value - KNOB_SENSITIVITY * (maxValue - minValue) * mouseRel.y); +} + +void Knob::onDragEnd() { + guiCursorUnlock(); +} diff --git a/src/widgets/Label.cpp b/src/widgets/Label.cpp new file mode 100644 index 00000000..c13af97d --- /dev/null +++ b/src/widgets/Label.cpp @@ -0,0 +1,6 @@ +#include "../5V.hpp" + + +void Label::draw(NVGcontext *vg) { + bndLabel(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, -1, text.c_str()); +} diff --git a/src/widgets/Light.cpp b/src/widgets/Light.cpp new file mode 100644 index 00000000..844d3065 --- /dev/null +++ b/src/widgets/Light.cpp @@ -0,0 +1,35 @@ +#include "../5V.hpp" + + +void Light::draw(NVGcontext *vg) { + SpriteWidget::draw(vg); + + if (color.a == 0.0) + return; + // Draw glow + Vec c = box.getCenter(); + float radius = box.size.x / 2; + NVGcolor icol, ocol; + NVGpaint paint; + // Inner glow + icol = nvgRGBf(1.0, 1.0, 1.0); + icol.a = clampf(color.a, 0.0, 1.0); + ocol = color; + ocol.a = clampf(color.a, 0.0, 1.0); + paint = nvgRadialGradient(vg, c.x+1, c.y+3, 0, radius, icol, ocol); + nvgFillPaint(vg, paint); + nvgBeginPath(vg); + nvgCircle(vg, c.x, c.y, radius); + nvgFill(vg); + // Outer glow + icol = color; + icol.a = clampf(0.1 * color.a, 0.0, 1.0); + ocol = color; + ocol.a = 0.0; + float oradius = radius + 20; + paint = nvgRadialGradient(vg, c.x, c.y, radius, oradius, icol, ocol); + nvgFillPaint(vg, paint); + nvgBeginPath(vg); + nvgRect(vg, c.x - oradius, c.y - oradius, 2*oradius, 2*oradius); + nvgFill(vg); +} diff --git a/src/widgets/Menu.cpp b/src/widgets/Menu.cpp new file mode 100644 index 00000000..12bb0dbb --- /dev/null +++ b/src/widgets/Menu.cpp @@ -0,0 +1,27 @@ +#include "../5V.hpp" + + +void Menu::pushChild(Widget *child) { + child->box.pos = Vec(0, box.size.y); + addChild(child); + box.size.y += child->box.size.y; +} + +void Menu::draw(NVGcontext *vg) { + // Resize the width to the widest child + for (Widget *child : children) { + MenuEntry *menuEntry = dynamic_cast(child); + float width = menuEntry->computeMinWidth(vg); + if (width > box.size.x) { + box.size.x = width; + } + } + // Resize widths of children + for (Widget *child : children) { + child->box.size.x = box.size.x; + } + + bndMenuBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE); + + Widget::draw(vg); +} diff --git a/src/widgets/MenuEntry.cpp b/src/widgets/MenuEntry.cpp new file mode 100644 index 00000000..280fe631 --- /dev/null +++ b/src/widgets/MenuEntry.cpp @@ -0,0 +1,6 @@ +#include "../5V.hpp" + + +float MenuEntry::computeMinWidth(NVGcontext *vg) { + return bndLabelWidth(vg, -1, text.c_str()); +} diff --git a/src/widgets/MenuItem.cpp b/src/widgets/MenuItem.cpp new file mode 100644 index 00000000..33eedd1d --- /dev/null +++ b/src/widgets/MenuItem.cpp @@ -0,0 +1,26 @@ +#include "../5V.hpp" + + +void MenuItem::draw(NVGcontext *vg) { + bndMenuItem(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, state, -1, text.c_str()); +} + +void MenuItem::onMouseEnter() { + state = BND_HOVER; +} + +void MenuItem::onMouseLeave() { + state = BND_DEFAULT; +} + +void MenuItem::onMouseUp(int button) { + onAction(); + // Remove overlay from scene + // HACK + Widget *overlay = parent->parent; + assert(overlay); + if (overlay->parent) { + overlay->parent->removeChild(overlay); + } + delete overlay; +} diff --git a/src/widgets/MenuLabel.cpp b/src/widgets/MenuLabel.cpp new file mode 100644 index 00000000..48761522 --- /dev/null +++ b/src/widgets/MenuLabel.cpp @@ -0,0 +1,6 @@ +#include "../5V.hpp" + + +void MenuLabel::draw(NVGcontext *vg) { + bndMenuLabel(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, -1, text.c_str()); +} diff --git a/src/widgets/MenuOverlay.cpp b/src/widgets/MenuOverlay.cpp new file mode 100644 index 00000000..178959b9 --- /dev/null +++ b/src/widgets/MenuOverlay.cpp @@ -0,0 +1,10 @@ +#include "../5V.hpp" + + +void MenuOverlay::onMouseDown(int button) { + if (parent) { + parent->removeChild(this); + } + // Commit sudoku + delete this; +} diff --git a/src/widgets/ModuleWidget.cpp b/src/widgets/ModuleWidget.cpp new file mode 100644 index 00000000..84bc71c0 --- /dev/null +++ b/src/widgets/ModuleWidget.cpp @@ -0,0 +1,163 @@ +#include "../5V.hpp" + + +ModuleWidget::ModuleWidget(Module *module) { + this->module = module; + if (module) { + rackAddModule(module); + } +} + +ModuleWidget::~ModuleWidget() { + // Make sure WireWidget destructors are called *before* removing `module` from the rack. + disconnectPorts(); + if (module) { + rackRemoveModule(module); + delete module; + } +} + +json_t *ModuleWidget::toJson() { + json_t *root = json_object(); + + // plugin + json_object_set_new(root, "plugin", json_string(model->plugin->slug.c_str())); + // model + json_object_set_new(root, "model", json_string(model->slug.c_str())); + // pos + json_t *pos = json_pack("[f, f]", (double) box.pos.x, (double) box.pos.y); + json_object_set_new(root, "pos", pos); + // params + json_t *paramsJ = json_array(); + for (ParamWidget *paramWidget : params) { + json_t *paramJ = paramWidget->toJson(); + json_array_append_new(paramsJ, paramJ); + } + json_object_set_new(root, "params", paramsJ); + + return root; +} + +void ModuleWidget::fromJson(json_t *root) { + // pos + json_t *pos = json_object_get(root, "pos"); + double x, y; + json_unpack(pos, "[F, F]", &x, &y); + box.pos = Vec(x, y); + + // params + json_t *paramsJ = json_object_get(root, "params"); + size_t paramId; + json_t *paramJ; + json_array_foreach(paramsJ, paramId, paramJ) { + params[paramId]->fromJson(paramJ); + } +} + +void ModuleWidget::disconnectPorts() { + for (InputPort *input : inputs) { + input->disconnect(); + } + for (OutputPort *output : outputs) { + output->disconnect(); + } +} + +void ModuleWidget::resetParams() { + for (ParamWidget *param : params) { + param->setValue(param->defaultValue); + } +} + +void ModuleWidget::cloneParams(ModuleWidget *source) { + assert(params.size() == source->params.size()); + for (size_t i = 0; i < params.size(); i++) { + params[i]->setValue(source->params[i]->value); + } +} + +void ModuleWidget::draw(NVGcontext *vg) { + Widget::draw(vg); + bndBevel(vg, box.pos.x, box.pos.y, box.size.x, box.size.y); +} + +void ModuleWidget::onDragStart() { + dragPos = gMousePos.minus(getAbsolutePos()); +} + +void ModuleWidget::onDragMove(Vec mouseRel) { + requestedPos = gMousePos.minus(parent->getAbsolutePos()).minus(dragPos); + requested = true; +} + +void ModuleWidget::onDragEnd() { +} + +struct DisconnectPortsMenuItem : MenuItem { + ModuleWidget *moduleWidget; + void onAction() { + moduleWidget->disconnectPorts(); + } +}; + +struct ResetParamsMenuItem : MenuItem { + ModuleWidget *moduleWidget; + void onAction() { + moduleWidget->resetParams(); + } +}; + +struct CloneModuleMenuItem : MenuItem { + ModuleWidget *moduleWidget; + void onAction() { + // Create new module from model + ModuleWidget *clonedModuleWidget = moduleWidget->model->createModuleWidget(); + clonedModuleWidget->requestedPos = moduleWidget->box.pos; + clonedModuleWidget->requested = true; + clonedModuleWidget->cloneParams(moduleWidget); + gRackWidget->moduleContainer->addChild(clonedModuleWidget); + } +}; + +struct DeleteModuleMenuItem : MenuItem { + ModuleWidget *moduleWidget; + void onAction() { + gRackWidget->moduleContainer->removeChild(moduleWidget); + delete moduleWidget; + } +}; + +void ModuleWidget::onMouseDown(int button) { + if (button == GLFW_MOUSE_BUTTON_RIGHT) { + MenuOverlay *overlay = new MenuOverlay(); + Menu *menu = new Menu(); + menu->box.pos = gMousePos; + { + MenuLabel *menuLabel = new MenuLabel(); + menuLabel->text = model->plugin->name + ": " + model->name; + menu->pushChild(menuLabel); + + ResetParamsMenuItem *resetItem = new ResetParamsMenuItem(); + resetItem->text = "Initialize parameters"; + resetItem->moduleWidget = this; + menu->pushChild(resetItem); + + DisconnectPortsMenuItem *disconnectItem = new DisconnectPortsMenuItem(); + disconnectItem->text = "Disconnect wires"; + disconnectItem->moduleWidget = this; + menu->pushChild(disconnectItem); + + CloneModuleMenuItem *cloneItem = new CloneModuleMenuItem(); + cloneItem->text = "Clone"; + cloneItem->moduleWidget = this; + menu->pushChild(cloneItem); + + DeleteModuleMenuItem *deleteItem = new DeleteModuleMenuItem(); + deleteItem->text = "Delete"; + deleteItem->moduleWidget = this; + menu->pushChild(deleteItem); + } + overlay->addChild(menu); + gScene->addChild(overlay); + } +} diff --git a/src/widgets/OutputPort.cpp b/src/widgets/OutputPort.cpp new file mode 100644 index 00000000..88f17699 --- /dev/null +++ b/src/widgets/OutputPort.cpp @@ -0,0 +1,34 @@ +#include "../5V.hpp" + + +void OutputPort::draw(NVGcontext *vg) { + Port::draw(vg); + if (gRackWidget->activeWire && gRackWidget->activeWire->outputPort) { + Port::drawGlow(vg); + } +} + +void OutputPort::onDragStart() { + if (connectedWire) { + // Disconnect wire from this port, but set it as the active wire + connectedWire->outputPort = NULL; + connectedWire->updateWire(); + gRackWidget->activeWire = connectedWire; + connectedWire = NULL; + } + else { + connectedWire = new WireWidget(); + connectedWire->outputPort = this; + gRackWidget->wireContainer->addChild(connectedWire); + gRackWidget->activeWire = connectedWire; + } +} + +void OutputPort::onDragDrop(Widget *origin) { + if (connectedWire) return; + if (gRackWidget->activeWire) { + if (gRackWidget->activeWire->outputPort) return; + gRackWidget->activeWire->outputPort = this; + connectedWire = gRackWidget->activeWire; + } +} diff --git a/src/widgets/ParamWidget.cpp b/src/widgets/ParamWidget.cpp new file mode 100644 index 00000000..a12690ba --- /dev/null +++ b/src/widgets/ParamWidget.cpp @@ -0,0 +1,29 @@ +#include "../5V.hpp" + + +json_t *ParamWidget::toJson() { + json_t *paramJ = json_object(); + + json_t *valueJ = json_real(value); + json_object_set_new(paramJ, "value", valueJ); + + return paramJ; +} + +void ParamWidget::fromJson(json_t *root) { + json_t *valueJ = json_object_get(root, "value"); + setValue(json_number_value(valueJ)); +} + +void ParamWidget::onMouseDown(int button) { + if (button == GLFW_MOUSE_BUTTON_RIGHT) { + setValue(defaultValue); + } +} + +void ParamWidget::onChange() { + assert(moduleWidget); + assert(moduleWidget->module); + // moduleWidget->module->params[paramId] = value; + rackSetParamSmooth(moduleWidget->module, paramId, value); +} diff --git a/src/widgets/Port.cpp b/src/widgets/Port.cpp new file mode 100644 index 00000000..d315a4b3 --- /dev/null +++ b/src/widgets/Port.cpp @@ -0,0 +1,54 @@ +#include "../5V.hpp" + + +Port::Port() { + box.size = Vec(20, 20); + type = rand() % 5; +} + +Port::~Port() { + disconnect(); +} + +void Port::disconnect() { + if (connectedWire) { + gRackWidget->wireContainer->removeChild(connectedWire); + // On destruction, Wire automatically sets connectedWire to NULL + delete connectedWire; + } +} + +void Port::draw(NVGcontext *vg) { + Vec pos = box.pos.plus(Vec(-18, -18)); + int width, height; + int imageId = loadImage("res/port.png"); + nvgImageSize(vg, imageId, &width, &height); + float offsetY = type * width; + NVGpaint paint = nvgImagePattern(vg, pos.x, pos.y - offsetY, width, height, 0.0, imageId, 1.0); + nvgFillPaint(vg, paint); + nvgBeginPath(vg); + nvgRect(vg, pos.x, pos.y, width, width); + nvgFill(vg); +} + +void Port::drawGlow(NVGcontext *vg) { + Vec c = box.getCenter(); + NVGcolor icol = nvgRGBAf(1, 1, 1, 0.5); + NVGcolor ocol = nvgRGBAf(1, 1, 1, 0); + NVGpaint paint = nvgRadialGradient(vg, c.x, c.y, 0, 20, icol, ocol); + nvgFillPaint(vg, paint); + nvgBeginPath(vg); + nvgRect(vg, box.pos.x - 10, box.pos.y - 10, box.size.x + 20, box.size.y + 20); + nvgFill(vg); +} + +void Port::onDragEnd() { + WireWidget *w = gRackWidget->activeWire; + assert(w); + w->updateWire(); + if (!w->wire) { + gRackWidget->wireContainer->removeChild(w); + delete w; + } + gRackWidget->activeWire = NULL; +} diff --git a/src/widgets/QuantityWidget.cpp b/src/widgets/QuantityWidget.cpp new file mode 100644 index 00000000..53b7a319 --- /dev/null +++ b/src/widgets/QuantityWidget.cpp @@ -0,0 +1,17 @@ +#include "../5V.hpp" + + +void QuantityWidget::setValue(float value) { + this->value = clampf(value, minValue, maxValue); + onChange(); +} + +void QuantityWidget::setLimits(float minValue, float maxValue) { + this->minValue = minValue; + this->maxValue = maxValue; +} + +void QuantityWidget::setDefaultValue(float defaultValue) { + this->defaultValue = defaultValue; + setValue(defaultValue); +} diff --git a/src/widgets/RackWidget.cpp b/src/widgets/RackWidget.cpp new file mode 100644 index 00000000..375b8f20 --- /dev/null +++ b/src/widgets/RackWidget.cpp @@ -0,0 +1,264 @@ +#include "../5V.hpp" +#include + + +RackWidget::RackWidget() { + moduleContainer = new TranslucentWidget(); + addChild(moduleContainer); + + wireContainer = new TransparentWidget(); + addChild(wireContainer); +} + +void RackWidget::clear() { + activeWire = NULL; + wireContainer->clearChildren(); + moduleContainer->clearChildren(); +} + +json_t *RackWidget::toJson() { + // root + json_t *root = json_object(); + + // rack + json_t *rack = json_object(); + { + json_t *size = json_pack("[f, f]", (double) box.size.x, (double) box.size.y); + json_object_set_new(rack, "size", size); + } + json_object_set_new(root, "rack", rack); + + // modules + json_t *modulesJ = json_array(); + std::map moduleIds; + int moduleId = 0; + for (Widget *w : moduleContainer->children) { + ModuleWidget *moduleWidget = dynamic_cast(w); + assert(moduleWidget); + moduleIds[moduleWidget] = moduleId; + moduleId++; + // module + json_t *moduleJ = moduleWidget->toJson(); + json_array_append_new(modulesJ, moduleJ); + } + json_object_set_new(root, "modules", modulesJ); + + // wires + json_t *wires = json_array(); + for (Widget *w : wireContainer->children) { + WireWidget *wireWidget = dynamic_cast(w); + assert(wireWidget); + // wire + json_t *wire = json_object(); + { + int outputModuleId = moduleIds[wireWidget->outputPort->moduleWidget]; + int inputModuleId = moduleIds[wireWidget->inputPort->moduleWidget]; + json_object_set_new(wire, "outputModuleId", json_integer(outputModuleId)); + json_object_set_new(wire, "outputId", json_integer(wireWidget->outputPort->outputId)); + json_object_set_new(wire, "inputModuleId", json_integer(inputModuleId)); + json_object_set_new(wire, "inputId", json_integer(wireWidget->inputPort->inputId)); + } + json_array_append_new(wires, wire); + } + json_object_set_new(root, "wires", wires); + + return root; +} + +void RackWidget::fromJson(json_t *root) { + // TODO There's virtually no validation in here. Bad input will result in a crash. + // rack + json_t *rack = json_object_get(root, "rack"); + assert(rack); + { + // size + json_t *size = json_object_get(rack, "size"); + double width, height; + json_unpack(size, "[F, F]", &width, &height); + box.size = Vec(width, height); + } + + // modules + std::map moduleWidgets; + json_t *modulesJ = json_object_get(root, "modules"); + size_t moduleId; + json_t *moduleJ; + json_array_foreach(modulesJ, moduleId, moduleJ) { + // Get plugin + json_t *pluginSlugJ = json_object_get(moduleJ, "plugin"); + const char *pluginSlug = json_string_value(pluginSlugJ); + Plugin *plugin = NULL; + for (Plugin *p : gPlugins) { + if (p->slug == pluginSlug) { + plugin = p; + break; + } + } + assert(plugin); + + // Get model + json_t *modelSlug = json_object_get(moduleJ, "model"); + Model *model = NULL; + for (Model *m : plugin->models) { + if (m->slug == json_string_value(modelSlug)) { + model = m; + break; + } + } + assert(model); + + // Create ModuleWidget + ModuleWidget *moduleWidget = model->createModuleWidget(); + moduleWidget->fromJson(moduleJ); + moduleContainer->addChild(moduleWidget); + moduleWidgets[moduleId] = moduleWidget; + } + + // wires + json_t *wiresJ = json_object_get(root, "wires"); + size_t wireId; + json_t *wireJ; + json_array_foreach(wiresJ, wireId, wireJ) { + int outputModuleId, outputId; + int inputModuleId, inputId; + json_unpack(wireJ, "{s:i, s:i, s:i, s:i}", + "outputModuleId", &outputModuleId, "outputId", &outputId, + "inputModuleId", &inputModuleId, "inputId", &inputId); + // Get ports + ModuleWidget *outputModuleWidget = moduleWidgets[outputModuleId]; + assert(outputModuleWidget); + OutputPort *outputPort = outputModuleWidget->outputs[outputId]; + assert(outputPort); + ModuleWidget *inputModuleWidget = moduleWidgets[inputModuleId]; + assert(inputModuleWidget); + InputPort *inputPort = inputModuleWidget->inputs[inputId]; + assert(inputPort); + // Create WireWidget + WireWidget *wireWidget = new WireWidget(); + wireWidget->outputPort = outputPort; + wireWidget->inputPort = inputPort; + outputPort->connectedWire = wireWidget; + inputPort->connectedWire = wireWidget; + wireWidget->updateWire(); + // Add wire to rack + gRackWidget->wireContainer->addChild(wireWidget); + } +} + +void RackWidget::repositionModule(ModuleWidget *module) { + // Create possible positions + int x0 = roundf(module->requestedPos.x / 15); + int y0 = roundf(module->requestedPos.y / 380); + std::vector positions; + for (int y = maxi(0, y0 - 2); y < y0 + 2; y++) { + for (int x = maxi(0, x0 - 40); x < x0 + 40; x++) { + positions.push_back(Vec(x*15, y*380)); + } + } + + // Sort possible positions by distance to the requested position + Vec requestedPos = module->requestedPos; + std::sort(positions.begin(), positions.end(), [requestedPos](Vec a, Vec b) { + return a.minus(requestedPos).norm() < b.minus(requestedPos).norm(); + }); + + // Find a position that does not collide + for (Vec pos : positions) { + Rect newBox = Rect(pos, module->box.size); + bool collides = false; + for (Widget *child2 : moduleContainer->children) { + if (module == child2) continue; + if (newBox.intersects(child2->box)) { + collides = true; + break; + } + } + if (collides) continue; + + module->box.pos = pos; + break; + } +} + +void RackWidget::step() { + // Resize to be a bit larger than the ScrollWidget viewport + assert(parent); + assert(parent->parent); + Vec moduleSize = moduleContainer->getChildrenBoundingBox().getBottomRight(); + Vec viewportSize = parent->parent->box.size.minus(parent->box.pos); + box.size = moduleSize.max(viewportSize).plus(Vec(500, 500)); + + // Reposition modules + for (Widget *child : moduleContainer->children) { + ModuleWidget *module = dynamic_cast(child); + assert(module); + if (module->requested) { + repositionModule(module); + module->requested = false; + } + } + + Widget::step(); +} + +void RackWidget::draw(NVGcontext *vg) { + // Draw background + nvgBeginPath(vg); + nvgRect(vg, box.pos.x, box.pos.y, box.size.x, box.size.y); + NVGpaint paint; + { + int imageId = loadImage("res/wood.jpg"); + int imageWidth, imageHeight; + nvgImageSize(vg, imageId, &imageWidth, &imageHeight); + paint = nvgImagePattern(vg, box.pos.x, box.pos.y, imageWidth, imageHeight, 0.0, imageId, 1.0); + nvgFillPaint(vg, paint); + nvgFill(vg); + } + { + int imageId = loadImage("res/rackrails.png"); + int imageWidth, imageHeight; + nvgImageSize(vg, imageId, &imageWidth, &imageHeight); + paint = nvgImagePattern(vg, box.pos.x, box.pos.y, imageWidth, imageHeight, 0.0, imageId, 1.0); + nvgFillPaint(vg, paint); + nvgFill(vg); + } + + Widget::draw(vg); +} + +struct AddModuleMenuItem : MenuItem { + Model *model; + Vec modulePos; + void onAction() { + ModuleWidget *moduleWidget = model->createModuleWidget(); + moduleWidget->requestedPos = modulePos.minus(moduleWidget->box.getCenter()); + moduleWidget->requested = true; + gRackWidget->moduleContainer->addChild(moduleWidget); + } +}; + +void RackWidget::onMouseDown(int button) { + if (button == GLFW_MOUSE_BUTTON_RIGHT) { + // Get relative position of the click + Vec modulePos = gMousePos.minus(getAbsolutePos()); + + MenuOverlay *overlay = new MenuOverlay(); + Menu *menu = new Menu(); + menu->box.pos = gMousePos; + + MenuLabel *menuLabel = new MenuLabel(); + menuLabel->text = "Add Module"; + menu->pushChild(menuLabel); + for (Plugin *plugin : gPlugins) { + for (Model *model : plugin->models) { + AddModuleMenuItem *item = new AddModuleMenuItem(); + item->text = model->plugin->name + ": " + model->name; + item->model = model; + item->modulePos = modulePos; + menu->pushChild(item); + } + } + overlay->addChild(menu); + gScene->addChild(overlay); + } +} diff --git a/src/widgets/Scene.cpp b/src/widgets/Scene.cpp new file mode 100644 index 00000000..3d8efdb9 --- /dev/null +++ b/src/widgets/Scene.cpp @@ -0,0 +1,22 @@ +#include "../5V.hpp" + + +Scene::Scene() { + scrollWidget = new ScrollWidget(); + { + assert(!gRackWidget); + gRackWidget = new RackWidget(); + scrollWidget->container->addChild(gRackWidget); + } + addChild(scrollWidget); + + toolbar = new Toolbar(); + addChild(toolbar); + scrollWidget->box.pos.y = toolbar->box.size.y; +} + +void Scene::onResize() { + toolbar->box.size.x = box.size.x; + scrollWidget->box.size = box.size.minus(scrollWidget->box.pos); + scrollWidget->onResize(); +} diff --git a/src/widgets/Screw.cpp b/src/widgets/Screw.cpp new file mode 100644 index 00000000..b3853571 --- /dev/null +++ b/src/widgets/Screw.cpp @@ -0,0 +1,10 @@ +#include "../5V.hpp" + + +Screw::Screw() { + box.size = Vec(15, 15); + spriteOffset = Vec(-7, -7); + spriteSize = Vec(29, 29); + spriteFilename = "res/screw.png"; + index = rand() % 5; +} diff --git a/src/widgets/ScrollBar.cpp b/src/widgets/ScrollBar.cpp new file mode 100644 index 00000000..c8d73b7f --- /dev/null +++ b/src/widgets/ScrollBar.cpp @@ -0,0 +1,38 @@ +#include "../5V.hpp" + + +ScrollBar::ScrollBar() { + box.size.x = BND_SCROLLBAR_WIDTH; + box.size.y = BND_SCROLLBAR_HEIGHT; +} + +void ScrollBar::draw(NVGcontext *vg) { + float boxSize = (orientation == VERTICAL ? box.size.y : box.size.x); + float maxOffset = containerSize - boxSize; + float offset = containerOffset / maxOffset; + float size = boxSize / containerSize; + size = clampf(size, 0.0, 1.0); + bndScrollBar(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, state, offset, size); +} + +void ScrollBar::move(float delta) { + float boxSize = (orientation == VERTICAL ? box.size.y : box.size.x); + float maxOffset = containerSize - boxSize; + containerOffset += delta; + containerOffset = clampf(containerOffset, 0.0, maxOffset); +} + +void ScrollBar::onDragStart() { + state = BND_ACTIVE; + guiCursorLock(); +} + +void ScrollBar::onDragMove(Vec mouseRel) { + float delta = (orientation == VERTICAL ? mouseRel.y : mouseRel.x); + move(delta); +} + +void ScrollBar::onDragEnd() { + state = BND_DEFAULT; + guiCursorUnlock(); +} diff --git a/src/widgets/ScrollWidget.cpp b/src/widgets/ScrollWidget.cpp new file mode 100644 index 00000000..91ac5bc4 --- /dev/null +++ b/src/widgets/ScrollWidget.cpp @@ -0,0 +1,42 @@ +#include "../5V.hpp" + + +ScrollWidget::ScrollWidget() { + container = new Widget(); + addChild(container); + + hScrollBar = new ScrollBar(); + hScrollBar->orientation = ScrollBar::HORIZONTAL; + addChild(hScrollBar); + + vScrollBar = new ScrollBar(); + vScrollBar->orientation = ScrollBar::VERTICAL; + addChild(vScrollBar); +} + +void ScrollWidget::draw(NVGcontext *vg) { + // Update the scrollbar sizes + Vec c = container->getChildrenBoundingBox().getBottomRight(); + hScrollBar->containerSize = c.x; + vScrollBar->containerSize = c.y; + + // Update the container's positions from the scrollbar offsets + container->box.pos = Vec(-hScrollBar->containerOffset, -vScrollBar->containerOffset).round(); + + Widget::draw(vg); +} + +void ScrollWidget::onResize() { + Vec b = Vec(box.size.x - vScrollBar->box.size.x, box.size.y - hScrollBar->box.size.y); + + hScrollBar->box.pos.y = b.y; + hScrollBar->box.size.x = b.x; + + vScrollBar->box.pos.x = b.x; + vScrollBar->box.size.y = b.y; +} + +void ScrollWidget::onScroll(Vec scrollRel) { + hScrollBar->move(scrollRel.x); + vScrollBar->move(scrollRel.y); +} diff --git a/src/widgets/Slider.cpp b/src/widgets/Slider.cpp new file mode 100644 index 00000000..067d2204 --- /dev/null +++ b/src/widgets/Slider.cpp @@ -0,0 +1,29 @@ +#include "../5V.hpp" + + +#define SLIDER_SENSITIVITY 0.001 + +Slider::Slider() { + box.size.y = BND_WIDGET_HEIGHT; +} + +void Slider::draw(NVGcontext *vg) { + float progress = mapf(value, minValue, maxValue, 0.0, 1.0); + char quantity[100]; + snprintf(quantity, 100, "%.3g%s", value, unit.c_str()); + bndSlider(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, progress, label.c_str(), quantity); +} + +void Slider::onDragStart() { + state = BND_ACTIVE; + guiCursorLock(); +} + +void Slider::onDragMove(Vec mouseRel) { + setValue(value + SLIDER_SENSITIVITY * (maxValue - minValue) * mouseRel.x); +} + +void Slider::onDragEnd() { + state = BND_DEFAULT; + guiCursorUnlock(); +} diff --git a/src/widgets/SpriteWidget.cpp b/src/widgets/SpriteWidget.cpp new file mode 100644 index 00000000..f56dc0de --- /dev/null +++ b/src/widgets/SpriteWidget.cpp @@ -0,0 +1,26 @@ +#include "../5V.hpp" + + +void SpriteWidget::draw(NVGcontext *vg) { + int imageId = loadImage(spriteFilename); + if (imageId < 0) { + // printf("Could not load image %s for SpriteWidget\n", spriteFilename.c_str()); + return; + } + + Vec pos = box.pos.plus(spriteOffset); + + int width, height; + nvgImageSize(vg, imageId, &width, &height); + int stride = width / spriteSize.x; + if (stride == 0) { + printf("Width of SpriteWidget is %d but spriteSize is %f\n", width, spriteSize.x); + return; + } + Vec offset = Vec((index % stride) * spriteSize.x, (index / stride) * spriteSize.y); + NVGpaint paint = nvgImagePattern(vg, pos.x - offset.x, pos.y - offset.y, width, height, 0.0, imageId, 1.0); + nvgFillPaint(vg, paint); + nvgBeginPath(vg); + nvgRect(vg, pos.x, pos.y, spriteSize.x, spriteSize.y); + nvgFill(vg); +} diff --git a/src/widgets/Toolbar.cpp b/src/widgets/Toolbar.cpp new file mode 100644 index 00000000..cec6285b --- /dev/null +++ b/src/widgets/Toolbar.cpp @@ -0,0 +1,171 @@ +#include "../5V.hpp" + +extern "C" { + #include "../lib/noc/noc_file_dialog.h" +} + + +static const char *filters = "JSON Patch\0*.json\0"; + + +struct NewItem : MenuItem { + void onAction() { + gRackWidget->clear(); + } +}; + +struct SaveItem : MenuItem { + void onAction() { + const char *path = noc_file_dialog_open(NOC_FILE_DIALOG_SAVE, filters, NULL, "Untitled.json"); + printf("Saving patch %s\n", path); + + if (path) { + FILE *file = fopen(path, "w"); + + json_t *root = gRackWidget->toJson(); + assert(root); + json_dumpf(root, file, JSON_INDENT(2)); + json_decref(root); + + fclose(file); + } + } +}; + +struct OpenItem : MenuItem { + void onAction() { + const char *path = noc_file_dialog_open(NOC_FILE_DIALOG_OPEN, filters, NULL, NULL); + printf("Loading patch %s\n", path); + + if (path) { + FILE *file = fopen(path, "r"); + + json_error_t error; + json_t *root = json_loadf(file, 0, &error); + if (root) { + gRackWidget->clear(); + gRackWidget->fromJson(root); + json_decref(root); + } + else { + printf("JSON parsing error at %s %d:%d %s\n", error.source, error.line, error.column, error.text); + } + + fclose(file); + } + } +}; + +struct FileChoice : ChoiceButton { + void onAction() { + MenuOverlay *overlay = new MenuOverlay(); + Menu *menu = new Menu(); + menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); + menu->box.size.x = box.size.x; + { + MenuItem *newItem = new NewItem(); + newItem->text = "New"; + menu->pushChild(newItem); + + MenuItem *openItem = new OpenItem(); + openItem->text = "Open"; + menu->pushChild(openItem); + + MenuItem *saveItem = new SaveItem(); + saveItem->text = "Save"; + menu->pushChild(saveItem); + + MenuItem *saveAsItem = new SaveItem(); + saveAsItem->text = "Save As"; + menu->pushChild(saveAsItem); + } + overlay->addChild(menu); + gScene->addChild(overlay); + } +}; + + +struct SampleRateItem : MenuItem { + float sampleRate; + void onAction() { + printf("\"\"\"\"\"\"\"\"switching\"\"\"\"\"\"\"\" sample rate to %f\n", sampleRate); + } +}; + +struct SampleRateChoice : ChoiceButton { + void onAction() { + MenuOverlay *overlay = new MenuOverlay(); + Menu *menu = new Menu(); + menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); + menu->box.size.x = box.size.x; + + float sampleRates[6] = {44100, 48000, 88200, 96000, 176400, 192000}; + for (int i = 0; i < 6; i++) { + SampleRateItem *item = new SampleRateItem(); + char text[100]; + snprintf(text, 100, "%.0f Hz", sampleRates[i]); + item->text = std::string(text); + item->sampleRate = sampleRates[i]; + menu->pushChild(item); + } + + overlay->addChild(menu); + gScene->addChild(overlay); + } +}; + + +Toolbar::Toolbar() { + float margin = 5; + box.size = Vec(1020, BND_WIDGET_HEIGHT + 2*margin); + + float xPos = margin; + { + Label *label = new Label(); + label->box.pos = Vec(xPos, margin); + label->text = "Rack v0.0.0 alpha"; + addChild(label); + xPos += 150; + } + + xPos += margin; + { + ChoiceButton *fileChoice = new FileChoice(); + fileChoice->box.pos = Vec(xPos, margin); + fileChoice->box.size.x = 100; + fileChoice->text = "File"; + addChild(fileChoice); + xPos += fileChoice->box.size.x; + } + + xPos += margin; + { + SampleRateChoice *srChoice = new SampleRateChoice(); + srChoice->box.pos = Vec(xPos, margin); + srChoice->box.size.x = 100; + // TODO Change to actual sample rate, e.g. 44100 Hz + srChoice->text = "Sample Rate"; + addChild(srChoice); + xPos += srChoice->box.size.x; + } + + xPos += margin; + { + wireOpacitySlider = new Slider(); + wireOpacitySlider->box.pos = Vec(xPos, margin); + wireOpacitySlider->box.size.x = 150; + wireOpacitySlider->label = "Wire opacity"; + wireOpacitySlider->unit = "%"; + wireOpacitySlider->setLimits(0.0, 100.0); + wireOpacitySlider->setDefaultValue(100.0); + addChild(wireOpacitySlider); + xPos += wireOpacitySlider->box.size.x; + } +} + +void Toolbar::draw(NVGcontext *vg) { + bndBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y); + bndBevel(vg, box.pos.x, box.pos.y, box.size.x, box.size.y); + + Widget::draw(vg); +} diff --git a/src/widgets/Tooltip.cpp b/src/widgets/Tooltip.cpp new file mode 100644 index 00000000..e2e55558 --- /dev/null +++ b/src/widgets/Tooltip.cpp @@ -0,0 +1,16 @@ +#include "../5V.hpp" + + +void Tooltip::step() { + // Follow the mouse + box.pos = gMousePos.minus(parent->getAbsolutePos()); + + // Wrap size to contents + // box.size = getChildrenBoundingBox().getBottomRight(); +} + + +void Tooltip::draw(NVGcontext *vg) { + bndTooltipBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y); + Widget::draw(vg); +} diff --git a/src/widgets/Widget.cpp b/src/widgets/Widget.cpp new file mode 100644 index 00000000..a3c7f59a --- /dev/null +++ b/src/widgets/Widget.cpp @@ -0,0 +1,89 @@ +#include "../5V.hpp" +#include + + +Widget::~Widget() { + // You should only delete orphaned widgets + assert(!parent); + // Stop dragging and hovering this widget + if (gHoveredWidget == this) + gHoveredWidget = NULL; + if (gDraggedWidget == this) + gDraggedWidget = NULL; + clearChildren(); +} + +Vec Widget::getAbsolutePos() { + // Recursively compute position offset from parents + if (!parent) { + return box.pos; + } + else { + return box.pos.plus(parent->getAbsolutePos()); + } +} + +Rect Widget::getChildrenBoundingBox() { + if (children.empty()) { + return Rect(); + } + + Vec topLeft = Vec(INFINITY, INFINITY); + Vec bottomRight = Vec(-INFINITY, -INFINITY); + for (Widget *child : children) { + topLeft = topLeft.min(child->box.pos); + bottomRight = bottomRight.max(child->box.getBottomRight()); + } + return Rect(topLeft, bottomRight.minus(topLeft)); +} + +void Widget::addChild(Widget *widget) { + assert(!widget->parent); + widget->parent = this; + children.push_back(widget); +} + +void Widget::removeChild(Widget *widget) { + assert(widget->parent == this); + auto it = std::find(children.begin(), children.end(), widget); + if (it != children.end()) { + children.erase(it); + widget->parent = NULL; + } +} + +void Widget::clearChildren() { + for (Widget *child : children) { + child->parent = NULL; + delete child; + } + children.clear(); +} + +void Widget::step() { + for (Widget *child : children) { + child->step(); + } +} + +void Widget::draw(NVGcontext *vg) { + nvgSave(vg); + nvgTranslate(vg, box.pos.x, box.pos.y); + for (Widget *child : children) { + child->draw(vg); + } + nvgRestore(vg); +} + +Widget *Widget::pick(Vec pos) { + if (!box.contains(pos)) + return NULL; + pos = pos.minus(box.pos); + for (auto it = children.rbegin(); it != children.rend(); it++) { + Widget *child = *it; + Widget *picked = child->pick(pos); + if (picked) + return picked; + } + return this; +} diff --git a/src/widgets/WireWidget.cpp b/src/widgets/WireWidget.cpp new file mode 100644 index 00000000..1d0746dd --- /dev/null +++ b/src/widgets/WireWidget.cpp @@ -0,0 +1,112 @@ +#include "../5V.hpp" + + +void drawWire(NVGcontext *vg, Vec pos1, Vec pos2, NVGcolor color) { + float dist = pos1.minus(pos2).norm(); + Vec slump = Vec(0, 100.0 + 0.5*dist); + Vec pos3 = pos1.plus(pos2).div(2).plus(slump); + + NVGcolor colorOutline = nvgRGBf(0, 0, 0); + + nvgLineJoin(vg, NVG_ROUND); + nvgStrokeWidth(vg, 4); + // Shadow + // Vec pos4 = pos3.plus(slump.mult(0.1)); + // NVGcolor colorShadow = nvgRGBAf(0, 0, 0, 0.2); + // nvgBeginPath(vg); + // nvgMoveTo(vg, pos1.x, pos1.y); + // nvgQuadTo(vg, pos4.x, pos4.y, pos2.x, pos2.y); + // nvgStrokeColor(vg, colorShadow); + // nvgStroke(vg); + + // Wire outline + nvgBeginPath(vg); + nvgMoveTo(vg, pos1.x, pos1.y); + nvgQuadTo(vg, pos3.x, pos3.y, pos2.x, pos2.y); + nvgStrokeColor(vg, colorOutline); + nvgStroke(vg); + + // Wire solid + nvgStrokeWidth(vg, 2); + nvgStrokeColor(vg, color); + nvgStroke(vg); +} + + +static NVGcolor wireColors[8] = { + nvgRGB(0x50, 0x50, 0x50), + nvgRGB(0xac, 0x41, 0x42), + nvgRGB(0x90, 0xa9, 0x59), + nvgRGB(0xf4, 0xbf, 0x75), + nvgRGB(0x6a, 0x9f, 0xb5), + nvgRGB(0xaa, 0x75, 0x9f), + nvgRGB(0x75, 0xb5, 0xaa), + nvgRGB(0xf5, 0xf5, 0xf5), +}; +static int wireColorId = 1; + + + +WireWidget::WireWidget() { + wireColorId = (wireColorId + 1) % 8; + color = wireColors[wireColorId]; +} + +WireWidget::~WireWidget() { + if (outputPort) { + outputPort->connectedWire = NULL; + outputPort = NULL; + } + if (inputPort) { + inputPort->connectedWire = NULL; + inputPort = NULL; + } + updateWire(); +} + +void WireWidget::updateWire() { + if (wire) { + rackDisconnectWire(wire); + delete wire; + wire = NULL; + } + if (inputPort && outputPort) { + wire = new Wire(); + wire->outputModule = outputPort->moduleWidget->module; + wire->outputId = outputPort->outputId; + wire->inputModule = inputPort->moduleWidget->module; + wire->inputId = inputPort->inputId; + rackConnectWire(wire); + } +} + +void WireWidget::draw(NVGcontext *vg) { + Vec outputPos, inputPos; + Vec absolutePos = getAbsolutePos(); + + if (outputPort) { + outputPos = Rect(outputPort->getAbsolutePos(), outputPort->box.size).getCenter(); + } + else { + outputPos = gMousePos; + } + if (inputPort) { + inputPos = Rect(inputPort->getAbsolutePos(), inputPort->box.size).getCenter(); + } + else { + inputPos = gMousePos; + } + + outputPos = outputPos.minus(absolutePos); + inputPos = inputPos.minus(absolutePos); + + bndNodePort(vg, outputPos.x, outputPos.y, BND_DEFAULT, color); + bndNodePort(vg, inputPos.x, inputPos.y, BND_DEFAULT, color); + nvgSave(vg); + float wireOpacity = gScene->toolbar->wireOpacitySlider->value / 100.0; + if (wireOpacity > 0.0) { + nvgGlobalAlpha(vg, wireOpacity); + drawWire(vg, outputPos, inputPos, color); + } + nvgRestore(vg); +}