@@ -2,7 +2,7 @@ Search before opening an issue to make sure your topic is not a duplicate. Delet | |||||
## For bug reports: | ## For bug reports: | ||||
Operating system, or "all" if known to exist on all three: | |||||
Operating system(s): | |||||
Version if official Rack release, commit hash and/or branch if from source: | Version if official Rack release, commit hash and/or branch if from source: | ||||
## For feature requests: | ## For feature requests: | ||||
@@ -1,4 +1,5 @@ | |||||
VERSION = 0.6.0dev | VERSION = 0.6.0dev | ||||
RACK_DIR = . | |||||
FLAGS += \ | FLAGS += \ | ||||
-Iinclude \ | -Iinclude \ | ||||
@@ -2,8 +2,11 @@ ifdef VERSION | |||||
FLAGS += -DVERSION=$(VERSION) | FLAGS += -DVERSION=$(VERSION) | ||||
endif | endif | ||||
RACK_DIR ?= . | |||||
include $(RACK_DIR)/arch.mk | |||||
# Generate dependency files alongside the object files | # Generate dependency files alongside the object files | ||||
FLAGS += -MMD | |||||
FLAGS += -MMD -MP | |||||
FLAGS += -g | FLAGS += -g | ||||
# Optimization | # Optimization | ||||
FLAGS += -O3 -march=nocona -ffast-math -fno-finite-math-only | FLAGS += -O3 -march=nocona -ffast-math -fno-finite-math-only | ||||
@@ -17,7 +20,6 @@ CXXFLAGS += -std=c++11 | |||||
ifeq ($(ARCH), lin) | ifeq ($(ARCH), lin) | ||||
FLAGS += -DARCH_LIN | FLAGS += -DARCH_LIN | ||||
endif | endif | ||||
ifeq ($(ARCH), mac) | ifeq ($(ARCH), mac) | ||||
FLAGS += -DARCH_MAC | FLAGS += -DARCH_MAC | ||||
CXXFLAGS += -stdlib=libc++ | CXXFLAGS += -stdlib=libc++ | ||||
@@ -26,7 +28,6 @@ ifeq ($(ARCH), mac) | |||||
FLAGS += $(MAC_SDK_FLAGS) | FLAGS += $(MAC_SDK_FLAGS) | ||||
LDFLAGS += $(MAC_SDK_FLAGS) | LDFLAGS += $(MAC_SDK_FLAGS) | ||||
endif | endif | ||||
ifeq ($(ARCH), win) | ifeq ($(ARCH), win) | ||||
FLAGS += -DARCH_WIN | FLAGS += -DARCH_WIN | ||||
FLAGS += -D_USE_MATH_DEFINES | FLAGS += -D_USE_MATH_DEFINES | ||||
@@ -112,7 +112,7 @@ $(openssl): | |||||
$(UNTAR) openssl-1.1.0g.tar.gz | $(UNTAR) openssl-1.1.0g.tar.gz | ||||
cd openssl-1.1.0g && ./config --prefix="$(LOCAL)" | cd openssl-1.1.0g && ./config --prefix="$(LOCAL)" | ||||
$(MAKE) -C openssl-1.1.0g | $(MAKE) -C openssl-1.1.0g | ||||
$(MAKE) -C openssl-1.1.0g install | |||||
$(MAKE) -C openssl-1.1.0g install_sw | |||||
$(libcurl): $(openssl) | $(libcurl): $(openssl) | ||||
$(WGET) https://github.com/curl/curl/releases/download/curl-7_56_0/curl-7.56.0.tar.gz | $(WGET) https://github.com/curl/curl/releases/download/curl-7_56_0/curl-7.56.0.tar.gz | ||||
@@ -145,7 +145,7 @@ ifeq ($(ARCH),win) | |||||
RTAUDIO_FLAGS += -DAUDIO_WINDOWS_DS=ON -DAUDIO_WINDOWS_WASAPI=ON -DAUDIO_WINDOWS_ASIO=ON | RTAUDIO_FLAGS += -DAUDIO_WINDOWS_DS=ON -DAUDIO_WINDOWS_WASAPI=ON -DAUDIO_WINDOWS_ASIO=ON | ||||
endif | endif | ||||
ifeq ($(ARCH),lin) | ifeq ($(ARCH),lin) | ||||
RTAUDIO_FLAGS += -DAUDIO_LINUX_ALSA=ON -DAUDIO_LINUX_PULSE=ON | |||||
RTAUDIO_FLAGS += -DAUDIO_LINUX_ALSA=ON | |||||
endif | endif | ||||
ifdef RTAUDIO_ALL_APIS | ifdef RTAUDIO_ALL_APIS | ||||
@@ -153,7 +153,7 @@ ifeq ($(ARCH),mac) | |||||
RTAUDIO_FLAGS += -DAUDIO_UNIX_JACK=ON | RTAUDIO_FLAGS += -DAUDIO_UNIX_JACK=ON | ||||
endif | endif | ||||
ifeq ($(ARCH),lin) | ifeq ($(ARCH),lin) | ||||
RTAUDIO_FLAGS += -DAUDIO_LINUX_JACK=ON | |||||
RTAUDIO_FLAGS += -DAUDIO_LINUX_PULSE=ON -DAUDIO_UNIX_JACK=ON | |||||
endif | endif | ||||
endif | endif | ||||
@@ -158,6 +158,8 @@ struct RackWidget : OpaqueWidget { | |||||
void openDialog(); | void openDialog(); | ||||
void saveDialog(); | void saveDialog(); | ||||
void saveAsDialog(); | void saveAsDialog(); | ||||
/** If `lastPath` is defined, ask the user to reload it */ | |||||
void revert(); | |||||
void savePatch(std::string filename); | void savePatch(std::string filename); | ||||
void loadPatch(std::string filename); | void loadPatch(std::string filename); | ||||
json_t *toJson(); | json_t *toJson(); | ||||
@@ -171,6 +173,7 @@ struct RackWidget : OpaqueWidget { | |||||
bool requestModuleBox(ModuleWidget *m, Rect box); | bool requestModuleBox(ModuleWidget *m, Rect box); | ||||
/** Moves a module to the closest non-colliding position */ | /** Moves a module to the closest non-colliding position */ | ||||
bool requestModuleBoxNearest(ModuleWidget *m, Rect box); | bool requestModuleBoxNearest(ModuleWidget *m, Rect box); | ||||
void step() override; | void step() override; | ||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
@@ -183,13 +186,6 @@ struct RackRail : TransparentWidget { | |||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
}; | }; | ||||
struct AddModuleWindow : Window { | |||||
Vec modulePos; | |||||
AddModuleWindow(); | |||||
void step() override; | |||||
}; | |||||
struct Panel : TransparentWidget { | struct Panel : TransparentWidget { | ||||
NVGcolor backgroundColor; | NVGcolor backgroundColor; | ||||
std::shared_ptr<Image> backgroundImage; | std::shared_ptr<Image> backgroundImage; | ||||
@@ -326,7 +322,7 @@ struct MomentarySwitch : virtual Switch { | |||||
// IO widgets | // IO widgets | ||||
//////////////////// | //////////////////// | ||||
struct LedDisplay : Widget { | |||||
struct LedDisplay : VirtualWidget { | |||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
}; | }; | ||||
@@ -488,7 +484,7 @@ struct Toolbar : OpaqueWidget { | |||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
}; | }; | ||||
struct PluginManagerWidget : Widget { | |||||
struct PluginManagerWidget : VirtualWidget { | |||||
Widget *loginWidget; | Widget *loginWidget; | ||||
Widget *manageWidget; | Widget *manageWidget; | ||||
Widget *downloadWidget; | Widget *downloadWidget; | ||||
@@ -524,8 +520,12 @@ extern RackScene *gRackScene; | |||||
extern RackWidget *gRackWidget; | extern RackWidget *gRackWidget; | ||||
extern Toolbar *gToolbar; | extern Toolbar *gToolbar; | ||||
void sceneInit(); | |||||
void sceneDestroy(); | |||||
void appInit(); | |||||
void appDestroy(); | |||||
void appModuleBrowserCreate(); | |||||
json_t *appModuleBrowserToJson(); | |||||
void appModuleBrowserFromJson(json_t *rootJ); | |||||
json_t *colorToJson(NVGcolor color); | json_t *colorToJson(NVGcolor color); | ||||
NVGcolor jsonToColor(json_t *colorJ); | NVGcolor jsonToColor(json_t *colorJ); | ||||
@@ -8,56 +8,76 @@ namespace rack { | |||||
/** A simple cyclic buffer. | /** A simple cyclic buffer. | ||||
S must be a power of 2. | S must be a power of 2. | ||||
push() is constant time O(1) | |||||
Thread-safe for single producers and consumers. | |||||
*/ | */ | ||||
template <typename T, int S> | |||||
template <typename T, size_t S> | |||||
struct RingBuffer { | struct RingBuffer { | ||||
T data[S]; | T data[S]; | ||||
int start = 0; | |||||
int end = 0; | |||||
size_t start = 0; | |||||
size_t end = 0; | |||||
int mask(int i) const { | |||||
return i & (S - 1); | |||||
size_t mask(size_t i) const { | |||||
return i & (S - 1); | |||||
} | } | ||||
void push(T t) { | void push(T t) { | ||||
int i = mask(end++); | |||||
size_t i = mask(end++); | |||||
data[i] = t; | data[i] = t; | ||||
} | } | ||||
void pushBuffer(const T *t, int n) { | |||||
size_t i = mask(end); | |||||
size_t e1 = i + n; | |||||
size_t e2 = (e1 < S) ? e1 : S; | |||||
memcpy(&data[i], t, sizeof(T) * (e2 - i)); | |||||
if (e1 > S) { | |||||
memcpy(data, &t[S - i], sizeof(T) * (e1 - S)); | |||||
} | |||||
end += n; | |||||
} | |||||
T shift() { | T shift() { | ||||
return data[mask(start++)]; | return data[mask(start++)]; | ||||
} | } | ||||
void shiftBuffer(T *t, size_t n) { | |||||
size_t i = mask(start); | |||||
size_t s1 = i + n; | |||||
size_t s2 = (s1 < S) ? s1 : S; | |||||
memcpy(t, &data[i], sizeof(T) * (s2 - i)); | |||||
if (s1 > S) { | |||||
memcpy(&t[S - i], data, sizeof(T) * (s1 - S)); | |||||
} | |||||
start += n; | |||||
} | |||||
void clear() { | void clear() { | ||||
start = end; | start = end; | ||||
} | } | ||||
bool empty() const { | bool empty() const { | ||||
return start >= end; | |||||
return start == end; | |||||
} | } | ||||
bool full() const { | bool full() const { | ||||
return end - start >= S; | |||||
return end - start == S; | |||||
} | } | ||||
int size() const { | |||||
size_t size() const { | |||||
return end - start; | return end - start; | ||||
} | } | ||||
int capacity() const { | |||||
size_t capacity() const { | |||||
return S - size(); | return S - size(); | ||||
} | } | ||||
}; | }; | ||||
/** A cyclic buffer which maintains a valid linear array of size S by keeping a copy of the buffer in adjacent memory. | /** A cyclic buffer which maintains a valid linear array of size S by keeping a copy of the buffer in adjacent memory. | ||||
S must be a power of 2. | S must be a power of 2. | ||||
push() is constant time O(2) relative to RingBuffer | |||||
Thread-safe for single producers and consumers? | |||||
*/ | */ | ||||
template <typename T, int S> | |||||
template <typename T, size_t S> | |||||
struct DoubleRingBuffer { | struct DoubleRingBuffer { | ||||
T data[S*2]; | T data[S*2]; | ||||
int start = 0; | |||||
int end = 0; | |||||
size_t start = 0; | |||||
size_t end = 0; | |||||
int mask(int i) const { | |||||
size_t mask(size_t i) const { | |||||
return i & (S - 1); | return i & (S - 1); | ||||
} | } | ||||
void push(T t) { | void push(T t) { | ||||
int i = mask(end++); | |||||
size_t i = mask(end++); | |||||
data[i] = t; | data[i] = t; | ||||
data[i + S] = t; | data[i + S] = t; | ||||
} | } | ||||
@@ -68,15 +88,15 @@ struct DoubleRingBuffer { | |||||
start = end; | start = end; | ||||
} | } | ||||
bool empty() const { | bool empty() const { | ||||
return start >= end; | |||||
return start == end; | |||||
} | } | ||||
bool full() const { | bool full() const { | ||||
return end - start >= S; | |||||
return end - start == S; | |||||
} | } | ||||
int size() const { | |||||
size_t size() const { | |||||
return end - start; | return end - start; | ||||
} | } | ||||
int capacity() const { | |||||
size_t capacity() const { | |||||
return S - size(); | return S - size(); | ||||
} | } | ||||
/** Returns a pointer to S consecutive elements for appending. | /** Returns a pointer to S consecutive elements for appending. | ||||
@@ -86,16 +106,16 @@ struct DoubleRingBuffer { | |||||
T *endData() { | T *endData() { | ||||
return &data[mask(end)]; | return &data[mask(end)]; | ||||
} | } | ||||
void endIncr(int n) { | |||||
int e = mask(end); | |||||
int e1 = e + n; | |||||
int e2 = min(e1, S); | |||||
void endIncr(size_t n) { | |||||
size_t e = mask(end); | |||||
size_t e1 = e + n; | |||||
size_t e2 = (e1 < S) ? e1 : S; | |||||
// Copy data forward | // Copy data forward | ||||
memcpy(data + S + e, data + e, sizeof(T) * (e2 - e)); | |||||
memcpy(&data[S + e], &data[e], sizeof(T) * (e2 - e)); | |||||
if (e1 > S) { | if (e1 > S) { | ||||
// Copy data backward from the doubled block to the main block | // Copy data backward from the doubled block to the main block | ||||
memcpy(data, data + S, sizeof(T) * (e1 - S)); | |||||
memcpy(data, &data[S], sizeof(T) * (e1 - S)); | |||||
} | } | ||||
end += n; | end += n; | ||||
} | } | ||||
@@ -105,7 +125,7 @@ struct DoubleRingBuffer { | |||||
const T *startData() const { | const T *startData() const { | ||||
return &data[mask(start)]; | return &data[mask(start)]; | ||||
} | } | ||||
void startIncr(int n) { | |||||
void startIncr(size_t n) { | |||||
start += n; | start += n; | ||||
} | } | ||||
}; | }; | ||||
@@ -114,17 +134,18 @@ struct DoubleRingBuffer { | |||||
The linear array of S elements are moved back to the start of the block once it outgrows past the end. | The linear array of S elements are moved back to the start of the block once it outgrows past the end. | ||||
This happens every N - S pushes, so the push() time is O(1 + S / (N - S)). | This happens every N - S pushes, so the push() time is O(1 + S / (N - S)). | ||||
For example, a float buffer of size 64 in a block of size 1024 is nearly as efficient as RingBuffer. | For example, a float buffer of size 64 in a block of size 1024 is nearly as efficient as RingBuffer. | ||||
Not thread-safe. | |||||
*/ | */ | ||||
template <typename T, int S, int N> | |||||
template <typename T, size_t S, size_t N> | |||||
struct AppleRingBuffer { | struct AppleRingBuffer { | ||||
T data[N]; | T data[N]; | ||||
int start = 0; | |||||
int end = 0; | |||||
size_t start = 0; | |||||
size_t end = 0; | |||||
void returnBuffer() { | void returnBuffer() { | ||||
// move end block to beginning | // move end block to beginning | ||||
// may overlap, but that's okay | |||||
int s = size(); | |||||
// may overlap, but memmove handles that correctly | |||||
size_t s = size(); | |||||
memmove(data, &data[start], sizeof(T) * s); | memmove(data, &data[start], sizeof(T) * s); | ||||
start = 0; | start = 0; | ||||
end = s; | end = s; | ||||
@@ -139,20 +160,20 @@ struct AppleRingBuffer { | |||||
return data[start++]; | return data[start++]; | ||||
} | } | ||||
bool empty() const { | bool empty() const { | ||||
return start >= end; | |||||
return start == end; | |||||
} | } | ||||
bool full() const { | bool full() const { | ||||
return end - start >= S; | |||||
return end - start == S; | |||||
} | } | ||||
int size() const { | |||||
size_t size() const { | |||||
return end - start; | return end - start; | ||||
} | } | ||||
int capacity() const { | |||||
size_t capacity() const { | |||||
return S - size(); | return S - size(); | ||||
} | } | ||||
/** Returns a pointer to S consecutive elements for appending, requesting to append n elements. | /** Returns a pointer to S consecutive elements for appending, requesting to append n elements. | ||||
*/ | */ | ||||
T *endData(int n) { | |||||
T *endData(size_t n) { | |||||
if (end + n > N) { | if (end + n > N) { | ||||
returnBuffer(); | returnBuffer(); | ||||
} | } | ||||
@@ -161,7 +182,7 @@ struct AppleRingBuffer { | |||||
/** Actually increments the end position | /** Actually increments the end position | ||||
Must be called after endData(), and `n` must be at most the `n` passed to endData() | Must be called after endData(), and `n` must be at most the `n` passed to endData() | ||||
*/ | */ | ||||
void endIncr(int n) { | |||||
void endIncr(size_t n) { | |||||
end += n; | end += n; | ||||
} | } | ||||
/** Returns a pointer to S consecutive elements for consumption | /** Returns a pointer to S consecutive elements for consumption | ||||
@@ -170,7 +191,7 @@ struct AppleRingBuffer { | |||||
const T *startData() const { | const T *startData() const { | ||||
return &data[start]; | return &data[start]; | ||||
} | } | ||||
void startIncr(int n) { | |||||
void startIncr(size_t n) { | |||||
// This is valid as long as n < S | // This is valid as long as n < S | ||||
start += n; | start += n; | ||||
} | } | ||||
@@ -18,7 +18,10 @@ struct Light { | |||||
void setBrightness(float brightness) { | void setBrightness(float brightness) { | ||||
value = (brightness > 0.f) ? brightness * brightness : 0.f; | value = (brightness > 0.f) ? brightness * brightness : 0.f; | ||||
} | } | ||||
void setBrightnessSmooth(float brightness); | |||||
/** Emulates slow fall (but immediate rise) of LED brightness. | |||||
`frames` rescales the timestep. For example, if your module calls this method every 16 frames, use 16.0. | |||||
*/ | |||||
void setBrightnessSmooth(float brightness, float frames = 1.f); | |||||
}; | }; | ||||
struct Input { | struct Input { | ||||
@@ -105,6 +105,8 @@ bool pluginIsDownloading(); | |||||
float pluginGetDownloadProgress(); | float pluginGetDownloadProgress(); | ||||
std::string pluginGetDownloadName(); | std::string pluginGetDownloadName(); | ||||
std::string pluginGetLoginStatus(); | std::string pluginGetLoginStatus(); | ||||
Plugin *pluginGetPlugin(std::string pluginSlug); | |||||
Model *pluginGetModel(std::string pluginSlug, std::string modelSlug); | |||||
extern std::list<Plugin*> gPlugins; | extern std::list<Plugin*> gPlugins; | ||||
@@ -9,12 +9,14 @@ namespace rack { | |||||
To see comments, turn word wrap on. I'm using inline comments so I can automatically sort the list when more tags are added. | To see comments, turn word wrap on. I'm using inline comments so I can automatically sort the list when more tags are added. | ||||
*/ | */ | ||||
enum ModelTag { | enum ModelTag { | ||||
NO_TAG, // Don't use this in `Model::create(...)`. Instead, just omit the tags entirely. | |||||
AMPLIFIER_TAG, | AMPLIFIER_TAG, | ||||
ARPEGGIATOR_TAG, | |||||
ATTENUATOR_TAG, | ATTENUATOR_TAG, | ||||
BLANK_TAG, | BLANK_TAG, | ||||
CHORUS_TAG, | CHORUS_TAG, | ||||
CLOCK_TAG, | |||||
CLOCK_MODULATOR_TAG, // Clock dividers, multipliers, etc. | CLOCK_MODULATOR_TAG, // Clock dividers, multipliers, etc. | ||||
CLOCK_TAG, | |||||
COMPRESSOR_TAG, | COMPRESSOR_TAG, | ||||
CONTROLLER_TAG, // Use only if the artist "performs" with this module. Knobs are not sufficient. Examples: on-screen keyboard, XY pad. | CONTROLLER_TAG, // Use only if the artist "performs" with this module. Knobs are not sufficient. Examples: on-screen keyboard, XY pad. | ||||
DELAY_TAG, | DELAY_TAG, | ||||
@@ -5,15 +5,51 @@ | |||||
namespace rack { | namespace rack { | ||||
//////////////////// | |||||
// Layouts (layouts.cpp) | |||||
//////////////////// | |||||
/** Positions children in a row/column based on their widths/heights */ | |||||
struct SequentialLayout : virtual Widget { | |||||
enum Orientation { | |||||
HORIZONTAL_ORIENTATION, | |||||
VERTICAL_ORIENTATION, | |||||
}; | |||||
Orientation orientation = HORIZONTAL_ORIENTATION; | |||||
enum Alignment { | |||||
LEFT_ALIGNMENT, | |||||
CENTER_ALIGNMENT, | |||||
RIGHT_ALIGNMENT, | |||||
}; | |||||
Alignment alignment = LEFT_ALIGNMENT; | |||||
/** Space between adjacent elements */ | |||||
float spacing = 0.0; | |||||
void step() override; | |||||
}; | |||||
//////////////////// | |||||
// Blendish UI elements | |||||
//////////////////// | |||||
struct Label : Widget { | |||||
struct Label : VirtualWidget { | |||||
std::string text; | std::string text; | ||||
enum Alignment { | |||||
LEFT_ALIGNMENT, | |||||
CENTER_ALIGNMENT, | |||||
RIGHT_ALIGNMENT, | |||||
}; | |||||
Alignment alignment = LEFT_ALIGNMENT; | |||||
Label() { | Label() { | ||||
box.size.y = BND_WIDGET_HEIGHT; | box.size.y = BND_WIDGET_HEIGHT; | ||||
} | } | ||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
}; | }; | ||||
struct List : OpaqueWidget { | |||||
void step() override; | |||||
void draw(NVGcontext *vg) override; | |||||
}; | |||||
/** Deletes itself from parent when clicked */ | /** Deletes itself from parent when clicked */ | ||||
struct MenuOverlay : OpaqueWidget { | struct MenuOverlay : OpaqueWidget { | ||||
void step() override; | void step() override; | ||||
@@ -33,7 +69,7 @@ struct Menu : OpaqueWidget { | |||||
box.size = Vec(0, 0); | box.size = Vec(0, 0); | ||||
} | } | ||||
~Menu(); | ~Menu(); | ||||
// Resizes menu and calls addChild() | |||||
/** Deprecated. Just use addChild(child) instead */ | |||||
void pushChild(Widget *child) DEPRECATED { | void pushChild(Widget *child) DEPRECATED { | ||||
addChild(child); | addChild(child); | ||||
} | } | ||||
@@ -47,7 +83,6 @@ struct MenuEntry : OpaqueWidget { | |||||
MenuEntry() { | MenuEntry() { | ||||
box.size = Vec(0, BND_WIDGET_HEIGHT); | box.size = Vec(0, BND_WIDGET_HEIGHT); | ||||
} | } | ||||
template <typename T = MenuEntry> | template <typename T = MenuEntry> | ||||
static T *create() { | static T *create() { | ||||
T *o = Widget::create<T>(Vec()); | T *o = Widget::create<T>(Vec()); | ||||
@@ -89,7 +124,7 @@ struct MenuItem : MenuEntry { | |||||
struct WindowOverlay : OpaqueWidget { | struct WindowOverlay : OpaqueWidget { | ||||
}; | }; | ||||
struct Window : OpaqueWidget { | |||||
struct WindowWidget : OpaqueWidget { | |||||
std::string title; | std::string title; | ||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
void onDragMove(EventDragMove &e) override; | void onDragMove(EventDragMove &e) override; | ||||
@@ -139,22 +174,7 @@ struct Slider : OpaqueWidget, QuantityWidget { | |||||
void onMouseDown(EventMouseDown &e) override; | void onMouseDown(EventMouseDown &e) override; | ||||
}; | }; | ||||
/** Parent must be a ScrollWidget */ | |||||
struct ScrollBar : OpaqueWidget { | |||||
enum { VERTICAL, HORIZONTAL } orientation; | |||||
BNDwidgetState state = BND_DEFAULT; | |||||
float offset = 0.0; | |||||
float size = 0.0; | |||||
ScrollBar() { | |||||
box.size = Vec(BND_SCROLLBAR_WIDTH, BND_SCROLLBAR_HEIGHT); | |||||
} | |||||
void draw(NVGcontext *vg) override; | |||||
void onDragStart(EventDragStart &e) override; | |||||
void onDragMove(EventDragMove &e) override; | |||||
void onDragEnd(EventDragEnd &e) override; | |||||
}; | |||||
struct ScrollBar; | |||||
/** Handles a container with ScrollBar */ | /** Handles a container with ScrollBar */ | ||||
struct ScrollWidget : OpaqueWidget { | struct ScrollWidget : OpaqueWidget { | ||||
Widget *container; | Widget *container; | ||||
@@ -163,19 +183,24 @@ struct ScrollWidget : OpaqueWidget { | |||||
Vec offset; | Vec offset; | ||||
ScrollWidget(); | ScrollWidget(); | ||||
void scrollTo(Rect r); | |||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
void step() override; | void step() override; | ||||
void onMouseMove(EventMouseMove &e) override; | void onMouseMove(EventMouseMove &e) override; | ||||
void onScroll(EventScroll &e) override; | void onScroll(EventScroll &e) override; | ||||
void onHoverKey(EventHoverKey &e) override; | |||||
}; | }; | ||||
struct TextField : OpaqueWidget { | struct TextField : OpaqueWidget { | ||||
std::string text; | std::string text; | ||||
std::string placeholder; | std::string placeholder; | ||||
bool multiline = false; | bool multiline = false; | ||||
int begin = 0; | |||||
int end = 0; | |||||
int dragPos = 0; | |||||
/** The index of the text cursor */ | |||||
int cursor = 0; | |||||
/** The index of the other end of the selection. | |||||
If nothing is selected, this is equal to `cursor`. | |||||
*/ | |||||
int selection = 0; | |||||
TextField() { | TextField() { | ||||
box.size.y = BND_WIDGET_HEIGHT; | box.size.y = BND_WIDGET_HEIGHT; | ||||
@@ -186,7 +211,10 @@ struct TextField : OpaqueWidget { | |||||
void onFocus(EventFocus &e) override; | void onFocus(EventFocus &e) override; | ||||
void onText(EventText &e) override; | void onText(EventText &e) override; | ||||
void onKey(EventKey &e) override; | void onKey(EventKey &e) override; | ||||
void insertText(std::string newText); | |||||
/** Inserts text at the cursor, replacing the selection if necessary */ | |||||
void insertText(std::string text); | |||||
/** Replaces the entire text */ | |||||
void setText(std::string text); | |||||
virtual int getTextPosition(Vec mousePos); | virtual int getTextPosition(Vec mousePos); | ||||
virtual void onTextChange() {} | virtual void onTextChange() {} | ||||
}; | }; | ||||
@@ -202,7 +230,7 @@ struct ProgressBar : QuantityWidget { | |||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
}; | }; | ||||
struct Tooltip : Widget { | |||||
struct Tooltip : VirtualWidget { | |||||
void step() override; | void step() override; | ||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
}; | }; | ||||
@@ -2,6 +2,7 @@ | |||||
#include "util/common.hpp" | #include "util/common.hpp" | ||||
#include <math.h> // for global namespace functions | #include <math.h> // for global namespace functions | ||||
#include <cmath> // for std::isfinite, etc | #include <cmath> // for std::isfinite, etc | ||||
#include <cstdlib> // for std::abs, etc | |||||
// Use a few standard math functions without std:: | // Use a few standard math functions without std:: | ||||
@@ -14,7 +15,7 @@ using std::isnormal; | |||||
namespace rack { | namespace rack { | ||||
//////////////////// | //////////////////// | ||||
// basic integer functions (suffixed with "i") | |||||
// basic integer functions | |||||
//////////////////// | //////////////////// | ||||
inline int min(int a, int b) { | inline int min(int a, int b) { | ||||
@@ -28,8 +29,8 @@ inline int max(int a, int b) { | |||||
/** Limits a value between a minimum and maximum | /** Limits a value between a minimum and maximum | ||||
Assumes min <= max | Assumes min <= max | ||||
*/ | */ | ||||
inline int clamp(int x, int minimum, int maximum) { | |||||
return min(max(x, minimum), maximum); | |||||
inline int clamp(int x, int min, int max) { | |||||
return rack::min(rack::max(x, min), max); | |||||
} | } | ||||
/** Euclidean modulus, always returns 0 <= mod < base for positive base. | /** Euclidean modulus, always returns 0 <= mod < base for positive base. | ||||
@@ -54,7 +55,7 @@ inline bool ispow2(int n) { | |||||
} | } | ||||
//////////////////// | //////////////////// | ||||
// basic float functions (suffixed with "f") | |||||
// basic float functions | |||||
//////////////////// | //////////////////// | ||||
/** Returns 1.f for positive numbers and -1.f for negative numbers (including positive/negative zero) */ | /** Returns 1.f for positive numbers and -1.f for negative numbers (including positive/negative zero) */ | ||||
@@ -74,11 +75,11 @@ inline bool isNear(float a, float b, float epsilon = 1.0e-6f) { | |||||
/** Limits a value between a minimum and maximum | /** Limits a value between a minimum and maximum | ||||
Assumes min <= max | Assumes min <= max | ||||
*/ | */ | ||||
inline float clamp(float x, float minimum, float maximum) { | |||||
return fminf(fmaxf(x, minimum), maximum); | |||||
inline float clamp(float x, float min, float max) { | |||||
return fminf(fmaxf(x, min), max); | |||||
} | } | ||||
/** Limits a value between a minimum and maximum | |||||
/** Limits a value between a min and max | |||||
If min > max, switches the two values | If min > max, switches the two values | ||||
*/ | */ | ||||
inline float clamp2(float x, float min, float max) { | inline float clamp2(float x, float min, float max) { | ||||
@@ -180,6 +181,7 @@ struct Vec { | |||||
return isfinite(x) && isfinite(y); | return isfinite(x) && isfinite(y); | ||||
} | } | ||||
Vec clamp(Rect bound); | Vec clamp(Rect bound); | ||||
Vec clamp2(Rect bound); | |||||
}; | }; | ||||
@@ -260,8 +262,14 @@ struct Rect { | |||||
inline Vec Vec::clamp(Rect bound) { | inline Vec Vec::clamp(Rect bound) { | ||||
return Vec( | return Vec( | ||||
clamp2(x, bound.pos.x, bound.pos.x + bound.size.x), | |||||
clamp2(y, bound.pos.y, bound.pos.y + bound.size.y)); | |||||
rack::clamp(x, bound.pos.x, bound.pos.x + bound.size.x), | |||||
rack::clamp(y, bound.pos.y, bound.pos.y + bound.size.y)); | |||||
} | |||||
inline Vec Vec::clamp2(Rect bound) { | |||||
return Vec( | |||||
rack::clamp2(x, bound.pos.x, bound.pos.x + bound.size.x), | |||||
rack::clamp2(y, bound.pos.y, bound.pos.y + bound.size.y)); | |||||
} | } | ||||
@@ -44,7 +44,9 @@ struct SVG { | |||||
// Base widget | // Base widget | ||||
//////////////////// | //////////////////// | ||||
/** A node in the 2D scene graph */ | |||||
/** A node in the 2D scene graph | |||||
Never inherit from Widget directly. Instead, inherit from VirtualWidget declared below. | |||||
*/ | |||||
struct Widget { | struct Widget { | ||||
/** Stores position and size */ | /** Stores position and size */ | ||||
Rect box = Rect(Vec(), Vec(INFINITY, INFINITY)); | Rect box = Rect(Vec(), Vec(INFINITY, INFINITY)); | ||||
@@ -152,7 +154,11 @@ struct Widget { | |||||
} | } | ||||
}; | }; | ||||
struct TransformWidget : Widget { | |||||
/** Instead of inheriting from Widget directly, inherit from VirtualWidget to guarantee that only one copy of Widget's member variables are used by each instance of the Widget hierarchy. | |||||
*/ | |||||
struct VirtualWidget : virtual Widget {}; | |||||
struct TransformWidget : VirtualWidget { | |||||
/** The transformation matrix */ | /** The transformation matrix */ | ||||
float transform[6]; | float transform[6]; | ||||
TransformWidget(); | TransformWidget(); | ||||
@@ -164,7 +170,7 @@ struct TransformWidget : Widget { | |||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
}; | }; | ||||
struct ZoomWidget : Widget { | |||||
struct ZoomWidget : VirtualWidget { | |||||
float zoom = 1.0; | float zoom = 1.0; | ||||
Vec getRelativeOffset(Vec v, Widget *relative) override; | Vec getRelativeOffset(Vec v, Widget *relative) override; | ||||
Rect getViewport(Rect r) override; | Rect getViewport(Rect r) override; | ||||
@@ -183,7 +189,7 @@ struct ZoomWidget : Widget { | |||||
//////////////////// | //////////////////// | ||||
/** Widget that does not respond to events */ | /** Widget that does not respond to events */ | ||||
struct TransparentWidget : virtual Widget { | |||||
struct TransparentWidget : VirtualWidget { | |||||
void onMouseDown(EventMouseDown &e) override {} | void onMouseDown(EventMouseDown &e) override {} | ||||
void onMouseUp(EventMouseUp &e) override {} | void onMouseUp(EventMouseUp &e) override {} | ||||
void onMouseMove(EventMouseMove &e) override {} | void onMouseMove(EventMouseMove &e) override {} | ||||
@@ -191,7 +197,7 @@ struct TransparentWidget : virtual Widget { | |||||
}; | }; | ||||
/** Widget that automatically responds to all mouse events but gives a chance for children to respond instead */ | /** Widget that automatically responds to all mouse events but gives a chance for children to respond instead */ | ||||
struct OpaqueWidget : virtual Widget { | |||||
struct OpaqueWidget : VirtualWidget { | |||||
void onMouseDown(EventMouseDown &e) override { | void onMouseDown(EventMouseDown &e) override { | ||||
Widget::onMouseDown(e); | Widget::onMouseDown(e); | ||||
if (!e.target) | if (!e.target) | ||||
@@ -216,7 +222,7 @@ struct OpaqueWidget : virtual Widget { | |||||
} | } | ||||
}; | }; | ||||
struct SpriteWidget : virtual Widget { | |||||
struct SpriteWidget : VirtualWidget { | |||||
Vec spriteOffset; | Vec spriteOffset; | ||||
Vec spriteSize; | Vec spriteSize; | ||||
std::shared_ptr<Image> spriteImage; | std::shared_ptr<Image> spriteImage; | ||||
@@ -224,7 +230,7 @@ struct SpriteWidget : virtual Widget { | |||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
}; | }; | ||||
struct SVGWidget : virtual Widget { | |||||
struct SVGWidget : VirtualWidget { | |||||
std::shared_ptr<SVG> svg; | std::shared_ptr<SVG> svg; | ||||
/** Sets the box size to the svg image size */ | /** Sets the box size to the svg image size */ | ||||
void wrap(); | void wrap(); | ||||
@@ -237,7 +243,7 @@ struct SVGWidget : virtual Widget { | |||||
When `dirty` is true, its children will be re-rendered on the next call to step() override. | When `dirty` is true, its children will be re-rendered on the next call to step() override. | ||||
Events are not passed to the underlying scene. | Events are not passed to the underlying scene. | ||||
*/ | */ | ||||
struct FramebufferWidget : virtual Widget { | |||||
struct FramebufferWidget : VirtualWidget { | |||||
/** Set this to true to re-render the children to the framebuffer the next time it is drawn */ | /** Set this to true to re-render the children to the framebuffer the next time it is drawn */ | ||||
bool dirty = true; | bool dirty = true; | ||||
/** A margin in pixels around the children in the framebuffer | /** A margin in pixels around the children in the framebuffer | ||||
@@ -257,7 +263,8 @@ struct FramebufferWidget : virtual Widget { | |||||
void onZoom(EventZoom &e) override; | void onZoom(EventZoom &e) override; | ||||
}; | }; | ||||
struct QuantityWidget : virtual Widget { | |||||
/** A Widget representing a float value */ | |||||
struct QuantityWidget : VirtualWidget { | |||||
float value = 0.0; | float value = 0.0; | ||||
float minValue = 0.0; | float minValue = 0.0; | ||||
float maxValue = 1.0; | float maxValue = 1.0; | ||||
@@ -4,7 +4,7 @@ | |||||
#include <thread> | #include <thread> | ||||
#include <mutex> | #include <mutex> | ||||
#include <condition_variable> | #include <condition_variable> | ||||
#include "core.hpp" | |||||
#include "Core.hpp" | |||||
#include "audio.hpp" | #include "audio.hpp" | ||||
#include "dsp/samplerate.hpp" | #include "dsp/samplerate.hpp" | ||||
#include "dsp/ringbuffer.hpp" | #include "dsp/ringbuffer.hpp" | ||||
@@ -58,7 +58,7 @@ struct AudioInterfaceIO : AudioIO { | |||||
if (numOutputs > 0) { | if (numOutputs > 0) { | ||||
std::unique_lock<std::mutex> lock(audioMutex); | std::unique_lock<std::mutex> lock(audioMutex); | ||||
auto cond = [&] { | auto cond = [&] { | ||||
return outputBuffer.size() >= length; | |||||
return (outputBuffer.size() >= (size_t) length); | |||||
}; | }; | ||||
if (audioCv.wait_for(lock, audioTimeout, cond)) { | if (audioCv.wait_for(lock, audioTimeout, cond)) { | ||||
// Consume audio block | // Consume audio block | ||||
@@ -187,7 +187,7 @@ void AudioInterface::step() { | |||||
// Wait until outputs are needed | // Wait until outputs are needed | ||||
std::unique_lock<std::mutex> lock(audioIO.engineMutex); | std::unique_lock<std::mutex> lock(audioIO.engineMutex); | ||||
auto cond = [&] { | auto cond = [&] { | ||||
return audioIO.outputBuffer.size() < audioIO.blockSize; | |||||
return (audioIO.outputBuffer.size() < (size_t) audioIO.blockSize); | |||||
}; | }; | ||||
if (audioIO.engineCv.wait_for(lock, audioTimeout, cond)) { | if (audioIO.engineCv.wait_for(lock, audioTimeout, cond)) { | ||||
// Push converted output | // Push converted output |
@@ -1,4 +1,4 @@ | |||||
#include "core.hpp" | |||||
#include "Core.hpp" | |||||
using namespace rack; | using namespace rack; | ||||
@@ -1,4 +1,4 @@ | |||||
#include "core.hpp" | |||||
#include "Core.hpp" | |||||
void init(rack::Plugin *p) { | void init(rack::Plugin *p) { |
@@ -1,4 +1,4 @@ | |||||
#include "core.hpp" | |||||
#include "Core.hpp" | |||||
#include "midi.hpp" | #include "midi.hpp" | ||||
#include "dsp/filter.hpp" | #include "dsp/filter.hpp" | ||||
@@ -1,4 +1,4 @@ | |||||
#include "core.hpp" | |||||
#include "Core.hpp" | |||||
#include "midi.hpp" | #include "midi.hpp" | ||||
#include "dsp/filter.hpp" | #include "dsp/filter.hpp" | ||||
@@ -1,4 +1,4 @@ | |||||
#include "core.hpp" | |||||
#include "Core.hpp" | |||||
#include "midi.hpp" | #include "midi.hpp" | ||||
#include "dsp/filter.hpp" | #include "dsp/filter.hpp" | ||||
@@ -1,4 +1,4 @@ | |||||
#include "core.hpp" | |||||
#include "Core.hpp" | |||||
using namespace rack; | using namespace rack; | ||||
@@ -1,4 +1,4 @@ | |||||
#include "core.hpp" | |||||
#include "Core.hpp" | |||||
#include "midi.hpp" | #include "midi.hpp" | ||||
@@ -1,292 +0,0 @@ | |||||
#include "app.hpp" | |||||
#include "plugin.hpp" | |||||
#include <thread> | |||||
#include <set> | |||||
#include <algorithm> | |||||
namespace rack { | |||||
static std::string sManufacturer; | |||||
static Model *sModel = NULL; | |||||
static std::string sFilter; | |||||
struct ListMenu : OpaqueWidget { | |||||
void draw(NVGcontext *vg) override { | |||||
Widget::draw(vg); | |||||
} | |||||
void step() override { | |||||
Widget::step(); | |||||
box.size.y = 0; | |||||
for (Widget *child : children) { | |||||
if (!child->visible) | |||||
continue; | |||||
// Increase height, set position of child | |||||
child->box.pos = Vec(0, box.size.y); | |||||
box.size.y += child->box.size.y; | |||||
child->box.size.x = box.size.x; | |||||
} | |||||
} | |||||
}; | |||||
struct UrlItem : MenuItem { | |||||
std::string url; | |||||
void onAction(EventAction &e) override { | |||||
std::thread t(openBrowser, url); | |||||
t.detach(); | |||||
} | |||||
}; | |||||
struct MetadataMenu : ListMenu { | |||||
Model *model = NULL; | |||||
void step() override { | |||||
if (model != sModel) { | |||||
model = sModel; | |||||
clearChildren(); | |||||
if (model) { | |||||
// Tag list | |||||
if (!model->tags.empty()) { | |||||
for (ModelTag tag : model->tags) { | |||||
addChild(construct<MenuLabel>(&MenuLabel::text, gTagNames[tag])); | |||||
} | |||||
addChild(construct<MenuEntry>()); | |||||
} | |||||
// Plugin name | |||||
std::string pluginName = model->plugin->slug; | |||||
if (!model->plugin->version.empty()) { | |||||
pluginName += " v"; | |||||
pluginName += model->plugin->version; | |||||
} | |||||
addChild(construct<MenuLabel>(&MenuLabel::text, pluginName)); | |||||
// Plugin metadata | |||||
if (!model->plugin->website.empty()) { | |||||
addChild(construct<UrlItem>(&MenuItem::text, "Website", &UrlItem::url, model->plugin->website)); | |||||
} | |||||
if (!model->plugin->manual.empty()) { | |||||
addChild(construct<UrlItem>(&MenuItem::text, "Manual", &UrlItem::url, model->plugin->manual)); | |||||
} | |||||
if (!model->plugin->path.empty()) { | |||||
addChild(construct<UrlItem>(&MenuItem::text, "Browse directory", &UrlItem::url, model->plugin->path)); | |||||
} | |||||
} | |||||
} | |||||
ListMenu::step(); | |||||
} | |||||
}; | |||||
static bool isModelMatch(Model *model, std::string search) { | |||||
// Build content string | |||||
std::string str; | |||||
str += model->manufacturer; | |||||
str += " "; | |||||
str += model->name; | |||||
str += " "; | |||||
str += model->slug; | |||||
for (ModelTag tag : model->tags) { | |||||
str += " "; | |||||
str += gTagNames[tag]; | |||||
} | |||||
str = lowercase(str); | |||||
search = lowercase(search); | |||||
return (str.find(search) != std::string::npos); | |||||
} | |||||
struct ModelItem : MenuItem { | |||||
Model *model; | |||||
void onAction(EventAction &e) override { | |||||
ModuleWidget *moduleWidget = model->createModuleWidget(); | |||||
gRackWidget->moduleContainer->addChild(moduleWidget); | |||||
// Move module nearest to the mouse position | |||||
Rect box; | |||||
box.size = moduleWidget->box.size; | |||||
AddModuleWindow *w = getAncestorOfType<AddModuleWindow>(); | |||||
box.pos = w->modulePos.minus(box.getCenter()); | |||||
gRackWidget->requestModuleBoxNearest(moduleWidget, box); | |||||
} | |||||
void onMouseEnter(EventMouseEnter &e) override { | |||||
sModel = model; | |||||
MenuItem::onMouseEnter(e); | |||||
} | |||||
}; | |||||
struct ModelMenu : ListMenu { | |||||
std::string manufacturer; | |||||
std::string filter; | |||||
void step() override { | |||||
if (manufacturer != sManufacturer) { | |||||
manufacturer = sManufacturer; | |||||
filter = ""; | |||||
clearChildren(); | |||||
addChild(construct<MenuLabel>(&MenuLabel::text, manufacturer)); | |||||
// Add models for the selected manufacturer | |||||
for (Plugin *plugin : gPlugins) { | |||||
for (Model *model : plugin->models) { | |||||
if (model->manufacturer == manufacturer) { | |||||
addChild(construct<ModelItem>(&MenuItem::text, model->name, &ModelItem::model, model)); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
if (filter != sFilter) { | |||||
filter = sFilter; | |||||
// Make all children invisible | |||||
for (Widget *child : children) { | |||||
child->visible = false; | |||||
} | |||||
// Make children with a matching model visible | |||||
for (Widget *child : children) { | |||||
ModelItem *item = dynamic_cast<ModelItem*>(child); | |||||
if (!item) | |||||
continue; | |||||
if (isModelMatch(item->model, filter)) { | |||||
item->visible = true; | |||||
} | |||||
} | |||||
} | |||||
ListMenu::step(); | |||||
} | |||||
}; | |||||
struct ManufacturerItem : MenuItem { | |||||
Model *model; | |||||
void onAction(EventAction &e) override { | |||||
sManufacturer = text; | |||||
e.consumed = false; | |||||
} | |||||
}; | |||||
struct ManufacturerMenu : ListMenu { | |||||
std::string filter; | |||||
ManufacturerMenu() { | |||||
addChild(construct<MenuLabel>(&MenuLabel::text, "Manufacturers")); | |||||
// Collect manufacturer names | |||||
std::set<std::string> manufacturers; | |||||
for (Plugin *plugin : gPlugins) { | |||||
for (Model *model : plugin->models) { | |||||
manufacturers.insert(model->manufacturer); | |||||
} | |||||
} | |||||
// Add menu item for each manufacturer name | |||||
for (std::string manufacturer : manufacturers) { | |||||
addChild(construct<ManufacturerItem>(&MenuItem::text, manufacturer)); | |||||
} | |||||
} | |||||
void step() override { | |||||
if (filter != sFilter) { | |||||
// Make all children invisible | |||||
for (Widget *child : children) { | |||||
child->visible = false; | |||||
} | |||||
// Make children with a matching model visible | |||||
for (Widget *child : children) { | |||||
MenuItem *item = dynamic_cast<MenuItem*>(child); | |||||
if (!item) | |||||
continue; | |||||
std::string manufacturer = item->text; | |||||
for (Plugin *plugin : gPlugins) { | |||||
for (Model *model : plugin->models) { | |||||
if (model->manufacturer == manufacturer) { | |||||
if (isModelMatch(model, sFilter)) { | |||||
item->visible = true; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
filter = sFilter; | |||||
} | |||||
ListMenu::step(); | |||||
} | |||||
}; | |||||
struct SearchModuleField : TextField { | |||||
void onTextChange() override { | |||||
sFilter = text; | |||||
} | |||||
}; | |||||
AddModuleWindow::AddModuleWindow() { | |||||
box.size = Vec(600, 300); | |||||
title = "Add module"; | |||||
float posY = BND_NODE_TITLE_HEIGHT; | |||||
// Search | |||||
SearchModuleField *searchField = new SearchModuleField(); | |||||
searchField->box.pos.y = posY; | |||||
posY += searchField->box.size.y; | |||||
searchField->box.size.x = box.size.x; | |||||
searchField->text = sFilter; | |||||
gFocusedWidget = searchField; | |||||
{ | |||||
EventFocus eFocus; | |||||
searchField->onFocus(eFocus); | |||||
searchField->onTextChange(); | |||||
} | |||||
addChild(searchField); | |||||
// Manufacturers | |||||
ManufacturerMenu *manufacturerMenu = new ManufacturerMenu(); | |||||
manufacturerMenu->box.size.x = 200; | |||||
ScrollWidget *manufacturerScroll = new ScrollWidget(); | |||||
manufacturerScroll->container->addChild(manufacturerMenu); | |||||
manufacturerScroll->box.pos = Vec(0, posY); | |||||
manufacturerScroll->box.size = Vec(200, box.size.y - posY); | |||||
addChild(manufacturerScroll); | |||||
// Models | |||||
ModelMenu *modelMenu = new ModelMenu(); | |||||
modelMenu->box.size.x = 200; | |||||
ScrollWidget *modelScroll = new ScrollWidget(); | |||||
modelScroll->container->addChild(modelMenu); | |||||
modelScroll->box.pos = Vec(200, posY); | |||||
modelScroll->box.size = Vec(200, box.size.y - posY); | |||||
addChild(modelScroll); | |||||
// Metadata | |||||
MetadataMenu *metadataMenu = new MetadataMenu(); | |||||
metadataMenu->box.size.x = 200; | |||||
ScrollWidget *metadataScroll = new ScrollWidget(); | |||||
metadataScroll->container->addChild(metadataMenu); | |||||
metadataScroll->box.pos = Vec(400, posY); | |||||
metadataScroll->box.size = Vec(200, box.size.y - posY); | |||||
addChild(metadataScroll); | |||||
} | |||||
void AddModuleWindow::step() { | |||||
Widget::step(); | |||||
} | |||||
} // namespace rack |
@@ -84,10 +84,11 @@ void LedDisplayTextField::draw(NVGcontext *vg) { | |||||
NVGcolor highlightColor = color; | NVGcolor highlightColor = color; | ||||
highlightColor.a = 0.5; | highlightColor.a = 0.5; | ||||
int cend = (this == gFocusedWidget) ? end : -1; | |||||
int begin = min(cursor, selection); | |||||
int end = (this == gFocusedWidget) ? max(cursor, selection) : -1; | |||||
bndIconLabelCaret(vg, textOffset.x, textOffset.y, | bndIconLabelCaret(vg, textOffset.x, textOffset.y, | ||||
box.size.x - 2*textOffset.x, box.size.y - 2*textOffset.y, | box.size.x - 2*textOffset.x, box.size.y - 2*textOffset.y, | ||||
-1, color, 12, text.c_str(), highlightColor, begin, cend); | |||||
-1, color, 12, text.c_str(), highlightColor, begin, end); | |||||
bndSetFont(gGuiFont->handle); | bndSetFont(gGuiFont->handle); | ||||
} | } | ||||
@@ -0,0 +1,559 @@ | |||||
#include "app.hpp" | |||||
#include "plugin.hpp" | |||||
#include "window.hpp" | |||||
#include <set> | |||||
#include <algorithm> | |||||
#define BND_LABEL_FONT_SIZE 13 | |||||
namespace rack { | |||||
static std::set<Model*> sFavoriteModels; | |||||
bool isMatch(std::string s, std::string search) { | |||||
s = lowercase(s); | |||||
search = lowercase(search); | |||||
return (s.find(search) != std::string::npos); | |||||
} | |||||
static bool isModelMatch(Model *model, std::string search) { | |||||
if (search.empty()) | |||||
return true; | |||||
std::string s; | |||||
s += model->plugin->slug; | |||||
s += " "; | |||||
s += model->manufacturer; | |||||
s += " "; | |||||
s += model->name; | |||||
s += " "; | |||||
s += model->slug; | |||||
for (ModelTag tag : model->tags) { | |||||
s += " "; | |||||
s += gTagNames[tag]; | |||||
} | |||||
return isMatch(s, search); | |||||
} | |||||
struct FavoriteRadioButton : RadioButton { | |||||
Model *model = NULL; | |||||
void onAction(EventAction &e) override; | |||||
}; | |||||
struct SeparatorItem : OpaqueWidget { | |||||
SeparatorItem() { | |||||
box.size.y = BND_WIDGET_HEIGHT; | |||||
} | |||||
void setText(std::string text) { | |||||
clearChildren(); | |||||
Label *label = Widget::create<Label>(Vec(0, 0)); | |||||
label->text = text; | |||||
addChild(label); | |||||
} | |||||
}; | |||||
struct BrowserListItem : OpaqueWidget { | |||||
bool selected = false; | |||||
BrowserListItem() { | |||||
box.size.y = 2 * BND_WIDGET_HEIGHT + 7; | |||||
} | |||||
void draw(NVGcontext *vg) override { | |||||
BNDwidgetState state = selected ? BND_HOVER : BND_DEFAULT; | |||||
bndMenuItem(vg, 0.0, 0.0, box.size.x, box.size.y, state, -1, ""); | |||||
Widget::draw(vg); | |||||
} | |||||
void onDragDrop(EventDragDrop &e) override { | |||||
if (e.origin != this) | |||||
return; | |||||
doAction(); | |||||
} | |||||
void doAction() { | |||||
EventAction eAction; | |||||
eAction.consumed = true; | |||||
onAction(eAction); | |||||
if (eAction.consumed) { | |||||
// deletes `this` | |||||
gScene->setOverlay(NULL); | |||||
} | |||||
} | |||||
void onMouseEnter(EventMouseEnter &e) override; | |||||
}; | |||||
struct ModelItem : BrowserListItem { | |||||
Model *model; | |||||
Label *manufacturerLabel; | |||||
void setModel(Model *model) { | |||||
clearChildren(); | |||||
assert(model); | |||||
this->model = model; | |||||
Label *nameLabel = Widget::create<Label>(Vec(0, 0)); | |||||
nameLabel->text = model->name; | |||||
addChild(nameLabel); | |||||
manufacturerLabel = Widget::create<Label>(Vec(0, 0)); | |||||
manufacturerLabel->alignment = Label::RIGHT_ALIGNMENT; | |||||
manufacturerLabel->text = model->manufacturer; | |||||
addChild(manufacturerLabel); | |||||
SequentialLayout *layout2 = Widget::create<SequentialLayout>(Vec(7, BND_WIDGET_HEIGHT)); | |||||
layout2->spacing = 0; | |||||
addChild(layout2); | |||||
FavoriteRadioButton *favoriteButton = new FavoriteRadioButton(); | |||||
favoriteButton->box.size.x = 20; | |||||
favoriteButton->label = "★"; | |||||
layout2->addChild(favoriteButton); | |||||
auto it = sFavoriteModels.find(model); | |||||
if (it != sFavoriteModels.end()) | |||||
favoriteButton->setValue(1); | |||||
favoriteButton->model = model; | |||||
// for (ModelTag tag : model->tags) { | |||||
// Button *tagButton = new Button(); | |||||
// tagButton->box.size.x = 120; | |||||
// tagButton->text = gTagNames[tag]; | |||||
// layout2->addChild(tagButton); | |||||
// } | |||||
Label *tagsLabel = new Label(); | |||||
int i = 0; | |||||
for (ModelTag tag : model->tags) { | |||||
if (i++ > 0) | |||||
tagsLabel->text += ", "; | |||||
tagsLabel->text += gTagNames[tag]; | |||||
} | |||||
layout2->addChild(tagsLabel); | |||||
} | |||||
void step() override { | |||||
BrowserListItem::step(); | |||||
manufacturerLabel->box.size.x = box.size.x - BND_SCROLLBAR_WIDTH; | |||||
} | |||||
void onAction(EventAction &e) override { | |||||
ModuleWidget *moduleWidget = model->createModuleWidget(); | |||||
gRackWidget->moduleContainer->addChild(moduleWidget); | |||||
// Move module nearest to the mouse position | |||||
moduleWidget->box.pos = gRackWidget->lastMousePos.minus(moduleWidget->box.size.div(2)); | |||||
gRackWidget->requestModuleBoxNearest(moduleWidget, moduleWidget->box); | |||||
} | |||||
}; | |||||
struct ManufacturerItem : BrowserListItem { | |||||
std::string manufacturer; | |||||
void setManufacturer(std::string manufacturer) { | |||||
clearChildren(); | |||||
this->manufacturer = manufacturer; | |||||
Label *manufacturerLabel = Widget::create<Label>(Vec(0, 0)); | |||||
if (manufacturer.empty()) | |||||
manufacturerLabel->text = "Show all modules"; | |||||
else | |||||
manufacturerLabel->text = manufacturer; | |||||
addChild(manufacturerLabel); | |||||
} | |||||
void onAction(EventAction &e) override; | |||||
}; | |||||
struct TagItem : BrowserListItem { | |||||
ModelTag tag; | |||||
void setTag(ModelTag tag) { | |||||
clearChildren(); | |||||
this->tag = tag; | |||||
Label *tagLabel = Widget::create<Label>(Vec(0, 0)); | |||||
if (tag == NO_TAG) | |||||
tagLabel->text = "Show all tags"; | |||||
else | |||||
tagLabel->text = gTagNames[tag]; | |||||
addChild(tagLabel); | |||||
} | |||||
void onAction(EventAction &e) override; | |||||
}; | |||||
struct ClearFilterItem : BrowserListItem { | |||||
ClearFilterItem() { | |||||
Label *label = Widget::create<Label>(Vec(0, 0)); | |||||
label->text = "Clear filter"; | |||||
addChild(label); | |||||
} | |||||
void onAction(EventAction &e) override; | |||||
}; | |||||
struct BrowserList : List { | |||||
int selected = 0; | |||||
void step() override { | |||||
// Count items | |||||
int n = 0; | |||||
for (Widget *child : children) { | |||||
BrowserListItem *item = dynamic_cast<BrowserListItem*>(child); | |||||
if (item) { | |||||
n++; | |||||
} | |||||
} | |||||
// If we have zero children, this result doesn't matter anyway. | |||||
selected = clamp(selected, 0, n - 1); | |||||
// Find and select item | |||||
int i = 0; | |||||
for (Widget *child : children) { | |||||
BrowserListItem *item = dynamic_cast<BrowserListItem*>(child); | |||||
if (item) { | |||||
item->selected = (i == selected); | |||||
i++; | |||||
} | |||||
} | |||||
List::step(); | |||||
} | |||||
void selectItem(Widget *w) { | |||||
int i = 0; | |||||
for (Widget *child : children) { | |||||
BrowserListItem *item = dynamic_cast<BrowserListItem*>(child); | |||||
if (item) { | |||||
if (child == w) { | |||||
selected = i; | |||||
break; | |||||
} | |||||
i++; | |||||
} | |||||
} | |||||
} | |||||
BrowserListItem *getSelectedItem() { | |||||
int i = 0; | |||||
for (Widget *child : children) { | |||||
BrowserListItem *item = dynamic_cast<BrowserListItem*>(child); | |||||
if (item) { | |||||
if (i == selected) { | |||||
return item; | |||||
} | |||||
i++; | |||||
} | |||||
} | |||||
return NULL; | |||||
} | |||||
void scrollSelected() { | |||||
BrowserListItem *item = getSelectedItem(); | |||||
if (item) { | |||||
ScrollWidget *parentScroll = dynamic_cast<ScrollWidget*>(parent->parent); | |||||
if (parentScroll) | |||||
parentScroll->scrollTo(item->box); | |||||
} | |||||
} | |||||
}; | |||||
struct ModuleBrowser; | |||||
struct SearchModuleField : TextField { | |||||
ModuleBrowser *moduleBrowser; | |||||
void onTextChange() override; | |||||
void onKey(EventKey &e) override; | |||||
}; | |||||
struct ModuleBrowser : OpaqueWidget { | |||||
SearchModuleField *searchField; | |||||
ScrollWidget *moduleScroll; | |||||
BrowserList *moduleList; | |||||
std::string manufacturerFilter; | |||||
ModelTag tagFilter = NO_TAG; | |||||
std::set<std::string> availableManufacturers; | |||||
std::set<ModelTag> availableTags; | |||||
ModuleBrowser() { | |||||
box.size.x = 400; | |||||
// Search | |||||
searchField = new SearchModuleField(); | |||||
searchField->box.size.x = box.size.x; | |||||
searchField->moduleBrowser = this; | |||||
addChild(searchField); | |||||
moduleList = new BrowserList(); | |||||
moduleList->box.size = Vec(box.size.x, 0.0); | |||||
// Module Scroll | |||||
moduleScroll = new ScrollWidget(); | |||||
moduleScroll->box.pos.y = searchField->box.size.y; | |||||
moduleScroll->box.size.x = box.size.x; | |||||
moduleScroll->container->addChild(moduleList); | |||||
addChild(moduleScroll); | |||||
// Collect manufacturers | |||||
for (Plugin *plugin : gPlugins) { | |||||
for (Model *model : plugin->models) { | |||||
// Insert manufacturer | |||||
if (!model->manufacturer.empty()) | |||||
availableManufacturers.insert(model->manufacturer); | |||||
// Insert tag | |||||
for (ModelTag tag : model->tags) { | |||||
if (tag != NO_TAG) | |||||
availableTags.insert(tag); | |||||
} | |||||
} | |||||
} | |||||
// Trigger search update | |||||
clearSearch(); | |||||
} | |||||
void clearSearch() { | |||||
searchField->setText(""); | |||||
} | |||||
bool isModelFiltered(Model *model) { | |||||
if (!manufacturerFilter.empty() && model->manufacturer != manufacturerFilter) | |||||
return false; | |||||
if (tagFilter != NO_TAG) { | |||||
auto it = std::find(model->tags.begin(), model->tags.end(), tagFilter); | |||||
if (it == model->tags.end()) | |||||
return false; | |||||
} | |||||
return true; | |||||
} | |||||
void refreshSearch() { | |||||
std::string search = searchField->text; | |||||
moduleList->clearChildren(); | |||||
moduleList->selected = 0; | |||||
// Favorites | |||||
{ | |||||
SeparatorItem *item = new SeparatorItem(); | |||||
item->setText("Favorites"); | |||||
moduleList->addChild(item); | |||||
} | |||||
for (Model *model : sFavoriteModels) { | |||||
if (isModelFiltered(model) && isModelMatch(model, search)) { | |||||
ModelItem *item = new ModelItem(); | |||||
item->setModel(model); | |||||
moduleList->addChild(item); | |||||
} | |||||
} | |||||
// Manufacturers | |||||
if (manufacturerFilter.empty() && tagFilter == NO_TAG) { | |||||
// Manufacturer items | |||||
{ | |||||
SeparatorItem *item = new SeparatorItem(); | |||||
item->setText("Manufacturers"); | |||||
moduleList->addChild(item); | |||||
} | |||||
for (std::string manufacturer : availableManufacturers) { | |||||
if (isMatch(manufacturer, search)) { | |||||
ManufacturerItem *item = new ManufacturerItem(); | |||||
item->setManufacturer(manufacturer); | |||||
moduleList->addChild(item); | |||||
} | |||||
} | |||||
// Tag items | |||||
{ | |||||
SeparatorItem *item = new SeparatorItem(); | |||||
item->setText("Tags"); | |||||
moduleList->addChild(item); | |||||
} | |||||
for (ModelTag tag : availableTags) { | |||||
if (isMatch(gTagNames[tag], search)) { | |||||
TagItem *item = new TagItem(); | |||||
item->setTag(tag); | |||||
moduleList->addChild(item); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
ClearFilterItem *item = new ClearFilterItem(); | |||||
moduleList->addChild(item); | |||||
} | |||||
// Models | |||||
if (!manufacturerFilter.empty() || tagFilter != NO_TAG || !search.empty()) { | |||||
{ | |||||
SeparatorItem *item = new SeparatorItem(); | |||||
item->setText("Modules"); | |||||
moduleList->addChild(item); | |||||
} | |||||
for (Plugin *plugin : gPlugins) { | |||||
for (Model *model : plugin->models) { | |||||
if (isModelFiltered(model) && isModelMatch(model, search)) { | |||||
ModelItem *item = new ModelItem(); | |||||
item->setModel(model); | |||||
moduleList->addChild(item); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
void step() override { | |||||
box.pos.y = 60; | |||||
box.size.y = parent->box.size.y - 2 * box.pos.y; | |||||
moduleScroll->box.size.y = box.size.y - moduleScroll->box.pos.y; | |||||
gFocusedWidget = searchField; | |||||
Widget::step(); | |||||
} | |||||
}; | |||||
// Implementations of inline methods above | |||||
void ManufacturerItem::onAction(EventAction &e) { | |||||
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
moduleBrowser->manufacturerFilter = manufacturer; | |||||
moduleBrowser->clearSearch(); | |||||
moduleBrowser->refreshSearch(); | |||||
e.consumed = false; | |||||
} | |||||
void TagItem::onAction(EventAction &e) { | |||||
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
moduleBrowser->tagFilter = tag; | |||||
moduleBrowser->clearSearch(); | |||||
moduleBrowser->refreshSearch(); | |||||
e.consumed = false; | |||||
} | |||||
void ClearFilterItem::onAction(EventAction &e) { | |||||
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
moduleBrowser->manufacturerFilter = ""; | |||||
moduleBrowser->tagFilter = NO_TAG; | |||||
moduleBrowser->clearSearch(); | |||||
moduleBrowser->refreshSearch(); | |||||
e.consumed = false; | |||||
} | |||||
void FavoriteRadioButton::onAction(EventAction &e) { | |||||
if (!model) | |||||
return; | |||||
if (value) { | |||||
sFavoriteModels.insert(model); | |||||
} | |||||
else { | |||||
auto it = sFavoriteModels.find(model); | |||||
if (it != sFavoriteModels.end()) | |||||
sFavoriteModels.erase(it); | |||||
} | |||||
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
if (moduleBrowser) | |||||
moduleBrowser->refreshSearch(); | |||||
} | |||||
void BrowserListItem::onMouseEnter(EventMouseEnter &e) { | |||||
BrowserList *list = getAncestorOfType<BrowserList>(); | |||||
list->selectItem(this); | |||||
} | |||||
void SearchModuleField::onTextChange() { | |||||
moduleBrowser->refreshSearch(); | |||||
} | |||||
void SearchModuleField::onKey(EventKey &e) { | |||||
switch (e.key) { | |||||
case GLFW_KEY_ESCAPE: { | |||||
gScene->setOverlay(NULL); | |||||
e.consumed = true; | |||||
return; | |||||
} break; | |||||
case GLFW_KEY_UP: { | |||||
moduleBrowser->moduleList->selected--; | |||||
moduleBrowser->moduleList->scrollSelected(); | |||||
e.consumed = true; | |||||
} break; | |||||
case GLFW_KEY_DOWN: { | |||||
moduleBrowser->moduleList->selected++; | |||||
moduleBrowser->moduleList->scrollSelected(); | |||||
e.consumed = true; | |||||
} break; | |||||
case GLFW_KEY_ENTER: { | |||||
BrowserListItem *item = moduleBrowser->moduleList->getSelectedItem(); | |||||
if (item) { | |||||
item->doAction(); | |||||
e.consumed = true; | |||||
return; | |||||
} | |||||
} break; | |||||
} | |||||
if (!e.consumed) { | |||||
TextField::onKey(e); | |||||
} | |||||
} | |||||
// Global functions | |||||
void appModuleBrowserCreate() { | |||||
MenuOverlay *overlay = new MenuOverlay(); | |||||
ModuleBrowser *moduleBrowser = new ModuleBrowser(); | |||||
overlay->addChild(moduleBrowser); | |||||
moduleBrowser->box.pos = gMousePos.minus(moduleBrowser->box.getCenter()); | |||||
gScene->setOverlay(overlay); | |||||
} | |||||
json_t *appModuleBrowserToJson() { | |||||
json_t *rootJ = json_object(); | |||||
json_t *favoritesJ = json_array(); | |||||
for (Model *model : sFavoriteModels) { | |||||
json_t *modelJ = json_object(); | |||||
json_object_set_new(modelJ, "plugin", json_string(model->plugin->slug.c_str())); | |||||
json_object_set_new(modelJ, "model", json_string(model->slug.c_str())); | |||||
json_array_append_new(favoritesJ, modelJ); | |||||
} | |||||
json_object_set_new(rootJ, "favorites", favoritesJ); | |||||
return rootJ; | |||||
} | |||||
void appModuleBrowserFromJson(json_t *rootJ) { | |||||
json_t *favoritesJ = json_object_get(rootJ, "favorites"); | |||||
if (favoritesJ) { | |||||
size_t i; | |||||
json_t *favoriteJ; | |||||
json_array_foreach(favoritesJ, i, favoriteJ) { | |||||
json_t *pluginJ = json_object_get(favoriteJ, "plugin"); | |||||
json_t *modelJ = json_object_get(favoriteJ, "model"); | |||||
if (!pluginJ || !modelJ) | |||||
continue; | |||||
std::string pluginSlug = json_string_value(pluginJ); | |||||
std::string modelSlug = json_string_value(modelJ); | |||||
Model *model = pluginGetModel(pluginSlug, modelSlug); | |||||
if (!model) | |||||
continue; | |||||
sFavoriteModels.insert(model); | |||||
} | |||||
} | |||||
} | |||||
} // namespace rack |
@@ -94,35 +94,34 @@ void RackScene::onHoverKey(EventHoverKey &e) { | |||||
if (windowIsModPressed() && !windowIsShiftPressed()) { | if (windowIsModPressed() && !windowIsShiftPressed()) { | ||||
gRackWidget->reset(); | gRackWidget->reset(); | ||||
e.consumed = true; | e.consumed = true; | ||||
return; | |||||
} | } | ||||
} break; | } break; | ||||
case GLFW_KEY_Q: { | case GLFW_KEY_Q: { | ||||
if (windowIsModPressed() && !windowIsShiftPressed()) { | if (windowIsModPressed() && !windowIsShiftPressed()) { | ||||
windowClose(); | windowClose(); | ||||
e.consumed = true; | e.consumed = true; | ||||
return; | |||||
} | } | ||||
} break; | } break; | ||||
case GLFW_KEY_O: { | case GLFW_KEY_O: { | ||||
if (windowIsModPressed() && !windowIsShiftPressed()) { | if (windowIsModPressed() && !windowIsShiftPressed()) { | ||||
gRackWidget->openDialog(); | gRackWidget->openDialog(); | ||||
e.consumed = true; | e.consumed = true; | ||||
return; | |||||
} | } | ||||
} break; | } break; | ||||
case GLFW_KEY_S: { | case GLFW_KEY_S: { | ||||
if (windowIsModPressed() && !windowIsShiftPressed()) { | if (windowIsModPressed() && !windowIsShiftPressed()) { | ||||
gRackWidget->saveDialog(); | gRackWidget->saveDialog(); | ||||
e.consumed = true; | e.consumed = true; | ||||
return; | |||||
} | } | ||||
if (windowIsModPressed() && windowIsShiftPressed()) { | if (windowIsModPressed() && windowIsShiftPressed()) { | ||||
gRackWidget->saveAsDialog(); | gRackWidget->saveAsDialog(); | ||||
e.consumed = true; | e.consumed = true; | ||||
return; | |||||
} | } | ||||
} break; | } break; | ||||
case GLFW_KEY_ENTER: { | |||||
appModuleBrowserCreate(); | |||||
e.consumed = true; | |||||
} break; | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -141,6 +141,14 @@ void RackWidget::loadPatch(std::string path) { | |||||
fclose(file); | fclose(file); | ||||
} | } | ||||
void RackWidget::revert() { | |||||
if (lastPath.empty()) | |||||
return; | |||||
if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Revert your patch to the last saved state?")) { | |||||
loadPatch(lastPath); | |||||
} | |||||
} | |||||
json_t *RackWidget::toJson() { | json_t *RackWidget::toJson() { | ||||
// root | // root | ||||
json_t *rootJ = json_object(); | json_t *rootJ = json_object(); | ||||
@@ -240,29 +248,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
std::string pluginSlug = json_string_value(pluginSlugJ); | std::string pluginSlug = json_string_value(pluginSlugJ); | ||||
std::string modelSlug = json_string_value(modelSlugJ); | std::string modelSlug = json_string_value(modelSlugJ); | ||||
// Search for plugin | |||||
Plugin *plugin = NULL; | |||||
for (Plugin *p : gPlugins) { | |||||
if (p->slug == pluginSlug) { | |||||
plugin = p; | |||||
break; | |||||
} | |||||
} | |||||
if (!plugin) { | |||||
message += stringf("Could not find plugin \"%s\" for module \"%s\"\n", pluginSlug.c_str(), modelSlug.c_str()); | |||||
continue; | |||||
} | |||||
// Search for model | |||||
Model *model = NULL; | |||||
for (Model *m : plugin->models) { | |||||
if (m->slug == modelSlug) { | |||||
model = m; | |||||
break; | |||||
} | |||||
} | |||||
Model *model = pluginGetModel(pluginSlug, modelSlug); | |||||
if (!model) { | if (!model) { | ||||
message += stringf("Could not find module \"%s\" in plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str()); | message += stringf("Could not find module \"%s\" in plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str()); | ||||
continue; | continue; | ||||
@@ -438,15 +424,7 @@ void RackWidget::onMouseDown(EventMouseDown &e) { | |||||
return; | return; | ||||
if (e.button == 1) { | if (e.button == 1) { | ||||
MenuOverlay *overlay = new MenuOverlay(); | |||||
AddModuleWindow *window = new AddModuleWindow(); | |||||
// Set center position | |||||
window->box.pos = gMousePos.minus(window->box.getCenter()); | |||||
window->modulePos = lastMousePos; | |||||
overlay->addChild(window); | |||||
gScene->setOverlay(overlay); | |||||
appModuleBrowserCreate(); | |||||
} | } | ||||
e.consumed = true; | e.consumed = true; | ||||
e.target = this; | e.target = this; | ||||
@@ -30,6 +30,12 @@ struct SaveAsItem : MenuItem { | |||||
} | } | ||||
}; | }; | ||||
struct RevertItem : MenuItem { | |||||
void onAction(EventAction &e) override { | |||||
gRackWidget->revert(); | |||||
} | |||||
}; | |||||
struct QuitItem : MenuItem { | struct QuitItem : MenuItem { | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
windowClose(); | windowClose(); | ||||
@@ -42,13 +48,12 @@ struct FileChoice : ChoiceButton { | |||||
menu->box.pos = getAbsoluteOffset(Vec(0, box.size.y)); | menu->box.pos = getAbsoluteOffset(Vec(0, box.size.y)); | ||||
menu->box.size.x = box.size.x; | menu->box.size.x = box.size.x; | ||||
{ | |||||
menu->addChild(construct<NewItem>(&MenuItem::text, "New", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+N")); | |||||
menu->addChild(construct<OpenItem>(&MenuItem::text, "Open", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+O")); | |||||
menu->addChild(construct<SaveItem>(&MenuItem::text, "Save", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+S")); | |||||
menu->addChild(construct<SaveAsItem>(&MenuItem::text, "Save as", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+Shift+S")); | |||||
menu->addChild(construct<QuitItem>(&MenuItem::text, "Quit", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+Q")); | |||||
} | |||||
menu->addChild(construct<NewItem>(&MenuItem::text, "New", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+N")); | |||||
menu->addChild(construct<OpenItem>(&MenuItem::text, "Open", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+O")); | |||||
menu->addChild(construct<SaveItem>(&MenuItem::text, "Save", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+S")); | |||||
menu->addChild(construct<SaveAsItem>(&MenuItem::text, "Save as", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+Shift+S")); | |||||
menu->addChild(construct<RevertItem>(&MenuItem::text, "Revert", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+R")); | |||||
menu->addChild(construct<QuitItem>(&MenuItem::text, "Quit", &MenuItem::rightText, WINDOW_MOD_KEY_NAME "+Q")); | |||||
} | } | ||||
}; | }; | ||||
@@ -95,96 +100,64 @@ struct EngineSampleRateChoice : ChoiceButton { | |||||
Toolbar::Toolbar() { | Toolbar::Toolbar() { | ||||
float margin = 5; | |||||
box.size.y = BND_WIDGET_HEIGHT + 2*margin; | |||||
float xPos = 0; | |||||
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; | |||||
{ | |||||
EngineSampleRateChoice *srChoice = new EngineSampleRateChoice(); | |||||
srChoice->box.pos = Vec(xPos, margin); | |||||
srChoice->box.size.x = 100; | |||||
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 = "Cable opacity"; | |||||
wireOpacitySlider->precision = 0; | |||||
wireOpacitySlider->unit = "%"; | |||||
wireOpacitySlider->setLimits(0.0, 100.0); | |||||
wireOpacitySlider->setDefaultValue(50.0); | |||||
addChild(wireOpacitySlider); | |||||
xPos += wireOpacitySlider->box.size.x; | |||||
} | |||||
xPos += margin; | |||||
{ | |||||
wireTensionSlider = new Slider(); | |||||
wireTensionSlider->box.pos = Vec(xPos, margin); | |||||
wireTensionSlider->box.size.x = 150; | |||||
wireTensionSlider->label = "Cable tension"; | |||||
wireTensionSlider->unit = ""; | |||||
wireTensionSlider->setLimits(0.0, 1.0); | |||||
wireTensionSlider->setDefaultValue(0.5); | |||||
addChild(wireTensionSlider); | |||||
xPos += wireTensionSlider->box.size.x; | |||||
} | |||||
xPos += margin; | |||||
{ | |||||
struct ZoomSlider : Slider { | |||||
void onAction(EventAction &e) override { | |||||
Slider::onAction(e); | |||||
gRackScene->zoomWidget->setZoom(roundf(value) / 100.0); | |||||
} | |||||
}; | |||||
zoomSlider = new ZoomSlider(); | |||||
zoomSlider->box.pos = Vec(xPos, margin); | |||||
zoomSlider->box.size.x = 150; | |||||
zoomSlider->precision = 0; | |||||
zoomSlider->label = "Zoom"; | |||||
zoomSlider->unit = "%"; | |||||
zoomSlider->setLimits(25.0, 200.0); | |||||
zoomSlider->setDefaultValue(100.0); | |||||
addChild(zoomSlider); | |||||
xPos += zoomSlider->box.size.x; | |||||
} | |||||
xPos += margin; | |||||
/* | |||||
{ | |||||
cpuUsageButton = new RadioButton(); | |||||
cpuUsageButton->box.pos = Vec(xPos, margin); | |||||
cpuUsageButton->box.size.x = 100; | |||||
cpuUsageButton->label = "CPU usage"; | |||||
addChild(cpuUsageButton); | |||||
xPos += cpuUsageButton->box.size.x; | |||||
} | |||||
xPos += margin; | |||||
*/ | |||||
box.size.y = BND_WIDGET_HEIGHT + 2*5; | |||||
SequentialLayout *layout = new SequentialLayout(); | |||||
layout->box.pos = Vec(5, 5); | |||||
layout->spacing = 5; | |||||
addChild(layout); | |||||
ChoiceButton *fileChoice = new FileChoice(); | |||||
fileChoice->box.size.x = 100; | |||||
fileChoice->text = "File"; | |||||
layout->addChild(fileChoice); | |||||
EngineSampleRateChoice *srChoice = new EngineSampleRateChoice(); | |||||
srChoice->box.size.x = 100; | |||||
layout->addChild(srChoice); | |||||
wireOpacitySlider = new Slider(); | |||||
wireOpacitySlider->box.size.x = 150; | |||||
wireOpacitySlider->label = "Cable opacity"; | |||||
wireOpacitySlider->precision = 0; | |||||
wireOpacitySlider->unit = "%"; | |||||
wireOpacitySlider->setLimits(0.0, 100.0); | |||||
wireOpacitySlider->setDefaultValue(50.0); | |||||
layout->addChild(wireOpacitySlider); | |||||
wireTensionSlider = new Slider(); | |||||
wireTensionSlider->box.size.x = 150; | |||||
wireTensionSlider->label = "Cable tension"; | |||||
wireTensionSlider->unit = ""; | |||||
wireTensionSlider->setLimits(0.0, 1.0); | |||||
wireTensionSlider->setDefaultValue(0.5); | |||||
layout->addChild(wireTensionSlider); | |||||
struct ZoomSlider : Slider { | |||||
void onAction(EventAction &e) override { | |||||
Slider::onAction(e); | |||||
gRackScene->zoomWidget->setZoom(roundf(value) / 100.0); | |||||
} | |||||
}; | |||||
zoomSlider = new ZoomSlider(); | |||||
zoomSlider->box.size.x = 150; | |||||
zoomSlider->precision = 0; | |||||
zoomSlider->label = "Zoom"; | |||||
zoomSlider->unit = "%"; | |||||
zoomSlider->setLimits(25.0, 200.0); | |||||
zoomSlider->setDefaultValue(100.0); | |||||
layout->addChild(zoomSlider); | |||||
/* | |||||
cpuUsageButton = new RadioButton(); | |||||
cpuUsageButton->box.size.x = 100; | |||||
cpuUsageButton->label = "CPU usage"; | |||||
layout->addChild(cpuUsageButton); | |||||
*/ | |||||
#if defined(RELEASE) | #if defined(RELEASE) | ||||
{ | |||||
Widget *pluginManager = new PluginManagerWidget(); | |||||
pluginManager->box.pos = Vec(xPos, margin); | |||||
addChild(pluginManager); | |||||
xPos += pluginManager->box.size.x; | |||||
} | |||||
Widget *pluginManager = new PluginManagerWidget(); | |||||
layout->addChild(pluginManager); | |||||
#endif | #endif | ||||
} | } | ||||
@@ -13,14 +13,13 @@ Toolbar *gToolbar = NULL; | |||||
RackScene *gRackScene = NULL; | RackScene *gRackScene = NULL; | ||||
void sceneInit() { | |||||
void appInit() { | |||||
gRackScene = new RackScene(); | gRackScene = new RackScene(); | ||||
gScene = gRackScene; | gScene = gRackScene; | ||||
} | } | ||||
void sceneDestroy() { | |||||
delete gScene; | |||||
gScene = NULL; | |||||
void appDestroy() { | |||||
delete gRackScene; | |||||
} | } | ||||
@@ -38,11 +38,11 @@ float Light::getBrightness() { | |||||
return sqrtf(fmaxf(0.f, value)); | return sqrtf(fmaxf(0.f, value)); | ||||
} | } | ||||
void Light::setBrightnessSmooth(float brightness) { | |||||
void Light::setBrightnessSmooth(float brightness, float frames) { | |||||
float v = (brightness > 0.f) ? brightness * brightness : 0.f; | float v = (brightness > 0.f) ? brightness * brightness : 0.f; | ||||
if (v < value) { | if (v < value) { | ||||
// Fade out light with lambda = framerate | // Fade out light with lambda = framerate | ||||
value += (v - value) * sampleTime * (60.f * 1.f); | |||||
value += (v - value) * sampleTime * frames * 60.f; | |||||
} | } | ||||
else { | else { | ||||
// Immediately illuminate light | // Immediately illuminate light | ||||
@@ -92,14 +92,14 @@ static void engineStep() { | |||||
// Step ports | // Step ports | ||||
for (Input &input : module->inputs) { | for (Input &input : module->inputs) { | ||||
if (input.active) { | if (input.active) { | ||||
float value = input.value / 10.0; | |||||
float value = input.value / 10.f; | |||||
input.plugLights[0].setBrightnessSmooth(value); | input.plugLights[0].setBrightnessSmooth(value); | ||||
input.plugLights[1].setBrightnessSmooth(-value); | input.plugLights[1].setBrightnessSmooth(-value); | ||||
} | } | ||||
} | } | ||||
for (Output &output : module->outputs) { | for (Output &output : module->outputs) { | ||||
if (output.active) { | if (output.active) { | ||||
float value = output.value / 10.0; | |||||
float value = output.value / 10.f; | |||||
output.plugLights[0].setBrightnessSmooth(value); | output.plugLights[0].setBrightnessSmooth(value); | ||||
output.plugLights[1].setBrightnessSmooth(-value); | output.plugLights[1].setBrightnessSmooth(-value); | ||||
} | } | ||||
@@ -34,20 +34,22 @@ int main(int argc, char* argv[]) { | |||||
pluginInit(); | pluginInit(); | ||||
engineInit(); | engineInit(); | ||||
windowInit(); | windowInit(); | ||||
sceneInit(); | |||||
appInit(); | |||||
settingsLoad(assetLocal("settings.json")); | settingsLoad(assetLocal("settings.json")); | ||||
std::string oldLastPath = gRackWidget->lastPath; | |||||
// To prevent launch crashes, if Rack crashes between now and 15 seconds from now, the "skipAutosaveOnLaunch" property will remain in settings.json, so that in the next launch, the broken autosave will not be loaded. | // To prevent launch crashes, if Rack crashes between now and 15 seconds from now, the "skipAutosaveOnLaunch" property will remain in settings.json, so that in the next launch, the broken autosave will not be loaded. | ||||
bool oldSkipAutosaveOnLaunch = skipAutosaveOnLaunch; | bool oldSkipAutosaveOnLaunch = skipAutosaveOnLaunch; | ||||
skipAutosaveOnLaunch = true; | skipAutosaveOnLaunch = true; | ||||
settingsSave(assetLocal("settings.json")); | settingsSave(assetLocal("settings.json")); | ||||
skipAutosaveOnLaunch = false; | skipAutosaveOnLaunch = false; | ||||
if (oldSkipAutosaveOnLaunch && osdialog_message(OSDIALOG_INFO, OSDIALOG_YES_NO, "Rack has recovered from a crash, likely caused by a faulty module in your patch. Would you like to clear your patch and start over?")) { | |||||
if (oldSkipAutosaveOnLaunch && osdialog_message(OSDIALOG_INFO, OSDIALOG_YES_NO, "Rack has recovered from a crash, possibly caused by a faulty module in your patch. Would you like to clear your patch and start over?")) { | |||||
// Do nothing. Empty patch is already loaded. | // Do nothing. Empty patch is already loaded. | ||||
} | } | ||||
else { | else { | ||||
gRackWidget->loadPatch(assetLocal("autosave.vcv")); | gRackWidget->loadPatch(assetLocal("autosave.vcv")); | ||||
} | } | ||||
gRackWidget->lastPath = oldLastPath; | |||||
engineStart(); | engineStart(); | ||||
windowRun(); | windowRun(); | ||||
@@ -55,7 +57,7 @@ int main(int argc, char* argv[]) { | |||||
gRackWidget->savePatch(assetLocal("autosave.vcv")); | gRackWidget->savePatch(assetLocal("autosave.vcv")); | ||||
settingsSave(assetLocal("settings.json")); | settingsSave(assetLocal("settings.json")); | ||||
sceneDestroy(); | |||||
appDestroy(); | |||||
windowDestroy(); | windowDestroy(); | ||||
engineDestroy(); | engineDestroy(); | ||||
pluginDestroy(); | pluginDestroy(); | ||||
@@ -475,5 +475,28 @@ std::string pluginGetLoginStatus() { | |||||
return loginStatus; | return loginStatus; | ||||
} | } | ||||
Plugin *pluginGetPlugin(std::string pluginSlug) { | |||||
for (Plugin *plugin : gPlugins) { | |||||
if (plugin->slug == pluginSlug) { | |||||
return plugin; | |||||
} | |||||
} | |||||
return NULL; | |||||
} | |||||
Model *pluginGetModel(std::string pluginSlug, std::string modelSlug) { | |||||
Plugin *plugin = pluginGetPlugin(pluginSlug); | |||||
if (plugin) { | |||||
for (Model *model : plugin->models) { | |||||
if (model->slug == modelSlug) { | |||||
return model; | |||||
} | |||||
} | |||||
} | |||||
return NULL; | |||||
} | |||||
} // namespace rack | } // namespace rack |
@@ -64,6 +64,9 @@ static json_t *settingsToJson() { | |||||
json_object_set_new(rootJ, "skipAutosaveOnLaunch", json_true()); | json_object_set_new(rootJ, "skipAutosaveOnLaunch", json_true()); | ||||
} | } | ||||
// moduleBrowser | |||||
json_object_set_new(rootJ, "moduleBrowser", appModuleBrowserToJson()); | |||||
return rootJ; | return rootJ; | ||||
} | } | ||||
@@ -123,9 +126,15 @@ static void settingsFromJson(json_t *rootJ) { | |||||
if (lastPathJ) | if (lastPathJ) | ||||
gRackWidget->lastPath = json_string_value(lastPathJ); | gRackWidget->lastPath = json_string_value(lastPathJ); | ||||
// skipAutosaveOnLaunch | |||||
json_t *skipAutosaveOnLaunchJ = json_object_get(rootJ, "skipAutosaveOnLaunch"); | json_t *skipAutosaveOnLaunchJ = json_object_get(rootJ, "skipAutosaveOnLaunch"); | ||||
if (skipAutosaveOnLaunchJ) | if (skipAutosaveOnLaunchJ) | ||||
skipAutosaveOnLaunch = json_boolean_value(skipAutosaveOnLaunchJ); | skipAutosaveOnLaunch = json_boolean_value(skipAutosaveOnLaunchJ); | ||||
// moduleBrowser | |||||
json_t * moduleBrowserJ = json_object_get(rootJ, "moduleBrowser"); | |||||
if (moduleBrowserJ) | |||||
appModuleBrowserFromJson(moduleBrowserJ); | |||||
} | } | ||||
@@ -10,6 +10,7 @@ std::string gTagNames[NUM_TAGS]; | |||||
void tagsInit() { | void tagsInit() { | ||||
gTagNames[AMPLIFIER_TAG] = "Amplifier/VCA"; | gTagNames[AMPLIFIER_TAG] = "Amplifier/VCA"; | ||||
gTagNames[ATTENUATOR_TAG] = "Attenuator"; | gTagNames[ATTENUATOR_TAG] = "Attenuator"; | ||||
gTagNames[ARPEGGIATOR_TAG] = "Arpeggiator"; | |||||
gTagNames[BLANK_TAG] = "Blank"; | gTagNames[BLANK_TAG] = "Blank"; | ||||
gTagNames[CHORUS_TAG] = "Chorus"; | gTagNames[CHORUS_TAG] = "Chorus"; | ||||
gTagNames[CLOCK_TAG] = "Clock"; | gTagNames[CLOCK_TAG] = "Clock"; | ||||
@@ -42,6 +43,7 @@ void tagsInit() { | |||||
gTagNames[OSCILLATOR_TAG] = "Oscillator/VCO"; | gTagNames[OSCILLATOR_TAG] = "Oscillator/VCO"; | ||||
gTagNames[PANNING_TAG] = "Panning"; | gTagNames[PANNING_TAG] = "Panning"; | ||||
gTagNames[PHASER_TAG] = "Phaser"; | gTagNames[PHASER_TAG] = "Phaser"; | ||||
gTagNames[PHYSICAL_MODELING_TAG] = "Physical Modeling"; | |||||
gTagNames[QUAD_TAG] = "Quad"; | gTagNames[QUAD_TAG] = "Quad"; | ||||
gTagNames[QUANTIZER_TAG] = "Quantizer"; | gTagNames[QUANTIZER_TAG] = "Quantizer"; | ||||
gTagNames[RANDOM_TAG] = "Random"; | gTagNames[RANDOM_TAG] = "Random"; | ||||
@@ -3,8 +3,18 @@ | |||||
namespace rack { | namespace rack { | ||||
void Label::draw(NVGcontext *vg) { | void Label::draw(NVGcontext *vg) { | ||||
bndLabel(vg, 0.0, 0.0, box.size.x, box.size.y, -1, text.c_str()); | |||||
float x = 0.0; | |||||
if (alignment == RIGHT_ALIGNMENT) { | |||||
x = box.size.x - bndLabelWidth(vg, -1, text.c_str()); | |||||
} | |||||
else if (alignment == CENTER_ALIGNMENT) { | |||||
x = (box.size.x - bndLabelWidth(vg, -1, text.c_str())) / 2.0; | |||||
} | |||||
bndLabel(vg, x, 0.0, box.size.x, box.size.y, -1, text.c_str()); | |||||
} | } | ||||
@@ -0,0 +1,30 @@ | |||||
#include "ui.hpp" | |||||
namespace rack { | |||||
void List::step() { | |||||
Widget::step(); | |||||
// Set positions of children | |||||
box.size.y = 0.0; | |||||
for (Widget *child : children) { | |||||
if (!child->visible) | |||||
continue; | |||||
// Increment height, set position of child | |||||
child->box.pos = Vec(0.0, box.size.y); | |||||
box.size.y += child->box.size.y; | |||||
// Resize width of child | |||||
child->box.size.x = box.size.x; | |||||
} | |||||
} | |||||
void List::draw(NVGcontext *vg) { | |||||
bndBackground(vg, 0.0, 0.0, box.size.x, box.size.y); | |||||
bndBevel(vg, 0.0, 0.0, box.size.x, box.size.y); | |||||
Widget::draw(vg); | |||||
} | |||||
} // namespace rack |
@@ -29,7 +29,7 @@ void Menu::step() { | |||||
for (Widget *child : children) { | for (Widget *child : children) { | ||||
if (!child->visible) | if (!child->visible) | ||||
continue; | continue; | ||||
// Increase height, set position of child | |||||
// Increment height, set position of child | |||||
child->box.pos = Vec(0, box.size.y); | child->box.pos = Vec(0, box.size.y); | ||||
box.size.y += child->box.size.y; | box.size.y += child->box.size.y; | ||||
// Increase width based on maximum width of child | // Increase width based on maximum width of child | ||||
@@ -24,7 +24,7 @@ void MenuItem::draw(NVGcontext *vg) { | |||||
} | } | ||||
void MenuItem::step() { | void MenuItem::step() { | ||||
// Add 10 more pixels because Retina measurements are sometimes too small | |||||
// Add 10 more pixels because measurements on high-DPI screens are sometimes too small for some reason | |||||
const float rightPadding = 10.0; | const float rightPadding = 10.0; | ||||
// HACK use gVg from the window. | // HACK use gVg from the window. | ||||
// All this does is inspect the font, so it shouldn't modify gVg and should work when called from a FramebufferWidget for example. | // All this does is inspect the font, so it shouldn't modify gVg and should work when called from a FramebufferWidget for example. | ||||
@@ -53,7 +53,7 @@ void MenuItem::onDragDrop(EventDragDrop &e) { | |||||
return; | return; | ||||
EventAction eAction; | EventAction eAction; | ||||
// TODO Perhaps remove this? It would require all onAction() methods to call this explicitly, which might be too annoying to change. | |||||
// Consume event by default, but allow action to un-consume it to prevent the menu from being removed. | |||||
eAction.consumed = true; | eAction.consumed = true; | ||||
onAction(eAction); | onAction(eAction); | ||||
if (eAction.consumed) { | if (eAction.consumed) { | ||||
@@ -1,4 +1,5 @@ | |||||
#include "ui.hpp" | #include "ui.hpp" | ||||
#include "window.hpp" | |||||
namespace rack { | namespace rack { | ||||
@@ -23,9 +24,19 @@ void MenuOverlay::onMouseDown(EventMouseDown &e) { | |||||
} | } | ||||
void MenuOverlay::onHoverKey(EventHoverKey &e) { | void MenuOverlay::onHoverKey(EventHoverKey &e) { | ||||
// Recurse children but consume the event | |||||
Widget::onHoverKey(e); | |||||
e.consumed = true; | |||||
switch (e.key) { | |||||
case GLFW_KEY_ESCAPE: { | |||||
gScene->setOverlay(NULL); | |||||
e.consumed = true; | |||||
return; | |||||
} break; | |||||
} | |||||
if (!e.consumed) { | |||||
// Recurse children but consume the event | |||||
Widget::onHoverKey(e); | |||||
e.consumed = true; | |||||
} | |||||
} | } | ||||
@@ -17,10 +17,10 @@ void RadioButton::onMouseLeave(EventMouseLeave &e) { | |||||
void RadioButton::onDragDrop(EventDragDrop &e) { | void RadioButton::onDragDrop(EventDragDrop &e) { | ||||
if (e.origin == this) { | if (e.origin == this) { | ||||
if (value == 0.0) | |||||
value = 1.0; | |||||
if (value) | |||||
setValue(0.0); | |||||
else | else | ||||
value = 0.0; | |||||
setValue(1.0); | |||||
EventAction eAction; | EventAction eAction; | ||||
onAction(eAction); | onAction(eAction); | ||||
@@ -1,32 +0,0 @@ | |||||
#include "ui.hpp" | |||||
#include "window.hpp" | |||||
namespace rack { | |||||
void ScrollBar::draw(NVGcontext *vg) { | |||||
bndScrollBar(vg, 0.0, 0.0, box.size.x, box.size.y, state, offset, size); | |||||
} | |||||
void ScrollBar::onDragStart(EventDragStart &e) { | |||||
state = BND_ACTIVE; | |||||
windowCursorLock(); | |||||
} | |||||
void ScrollBar::onDragMove(EventDragMove &e) { | |||||
ScrollWidget *scrollWidget = dynamic_cast<ScrollWidget*>(parent); | |||||
assert(scrollWidget); | |||||
if (orientation == HORIZONTAL) | |||||
scrollWidget->offset.x += e.mouseRel.x; | |||||
else | |||||
scrollWidget->offset.y += e.mouseRel.y; | |||||
} | |||||
void ScrollBar::onDragEnd(EventDragEnd &e) { | |||||
state = BND_DEFAULT; | |||||
windowCursorUnlock(); | |||||
} | |||||
} // namespace rack |
@@ -4,6 +4,47 @@ | |||||
namespace rack { | namespace rack { | ||||
/** Parent must be a ScrollWidget */ | |||||
struct ScrollBar : OpaqueWidget { | |||||
enum Orientation { | |||||
VERTICAL, | |||||
HORIZONTAL | |||||
}; | |||||
Orientation orientation; | |||||
BNDwidgetState state = BND_DEFAULT; | |||||
float offset = 0.0; | |||||
float size = 0.0; | |||||
ScrollBar() { | |||||
box.size = Vec(BND_SCROLLBAR_WIDTH, BND_SCROLLBAR_HEIGHT); | |||||
} | |||||
void draw(NVGcontext *vg) override { | |||||
bndScrollBar(vg, 0.0, 0.0, box.size.x, box.size.y, state, offset, size); | |||||
} | |||||
void onDragStart(EventDragStart &e) override { | |||||
state = BND_ACTIVE; | |||||
windowCursorLock(); | |||||
} | |||||
void onDragMove(EventDragMove &e) override { | |||||
ScrollWidget *scrollWidget = dynamic_cast<ScrollWidget*>(parent); | |||||
assert(scrollWidget); | |||||
if (orientation == HORIZONTAL) | |||||
scrollWidget->offset.x += e.mouseRel.x; | |||||
else | |||||
scrollWidget->offset.y += e.mouseRel.y; | |||||
} | |||||
void onDragEnd(EventDragEnd &e) override { | |||||
state = BND_DEFAULT; | |||||
windowCursorUnlock(); | |||||
} | |||||
}; | |||||
ScrollWidget::ScrollWidget() { | ScrollWidget::ScrollWidget() { | ||||
container = new Widget(); | container = new Widget(); | ||||
addChild(container); | addChild(container); | ||||
@@ -19,25 +60,29 @@ ScrollWidget::ScrollWidget() { | |||||
addChild(verticalScrollBar); | addChild(verticalScrollBar); | ||||
} | } | ||||
void ScrollWidget::scrollTo(Rect r) { | |||||
Rect bound = Rect::fromMinMax(r.getBottomRight().minus(box.size), r.pos); | |||||
offset = offset.clamp2(bound); | |||||
} | |||||
void ScrollWidget::draw(NVGcontext *vg) { | void ScrollWidget::draw(NVGcontext *vg) { | ||||
nvgScissor(vg, 0, 0, box.size.x, box.size.y); | nvgScissor(vg, 0, 0, box.size.x, box.size.y); | ||||
Widget::draw(vg); | Widget::draw(vg); | ||||
nvgResetScissor(vg); | nvgResetScissor(vg); | ||||
} | } | ||||
void ScrollWidget::step() { | void ScrollWidget::step() { | ||||
Widget::step(); | |||||
// Clamp scroll offset | // Clamp scroll offset | ||||
Vec containerCorner = container->getChildrenBoundingBox().getBottomRight(); | Vec containerCorner = container->getChildrenBoundingBox().getBottomRight(); | ||||
offset = offset.clamp(Rect(Vec(0, 0), containerCorner.minus(box.size))); | |||||
// Resize scroll bars | |||||
Vec inner = Vec(box.size.x - verticalScrollBar->box.size.x, box.size.y - horizontalScrollBar->box.size.y); | |||||
horizontalScrollBar->box.pos.y = inner.y; | |||||
horizontalScrollBar->box.size.x = inner.x; | |||||
verticalScrollBar->box.pos.x = inner.x; | |||||
verticalScrollBar->box.size.y = inner.y; | |||||
Rect containerBox = Rect(Vec(0, 0), containerCorner.minus(box.size)); | |||||
offset = offset.clamp(containerBox); | |||||
// Lock offset to top/left if no scrollbar will display | |||||
if (containerBox.size.x < 0.0) | |||||
offset.x = 0.0; | |||||
if (containerBox.size.y < 0.0) | |||||
offset.y = 0.0; | |||||
// Update the container's positions from the offset | // Update the container's positions from the offset | ||||
container->box.pos = offset.neg().round(); | container->box.pos = offset.neg().round(); | ||||
@@ -47,14 +92,19 @@ void ScrollWidget::step() { | |||||
Vec scrollbarOffset = offset.div(viewportSize.minus(box.size)); | Vec scrollbarOffset = offset.div(viewportSize.minus(box.size)); | ||||
Vec scrollbarSize = box.size.div(viewportSize); | Vec scrollbarSize = box.size.div(viewportSize); | ||||
horizontalScrollBar->offset = scrollbarOffset.x; | |||||
horizontalScrollBar->size = scrollbarSize.x; | |||||
horizontalScrollBar->visible = (0.0 < scrollbarSize.x && scrollbarSize.x < 1.0); | horizontalScrollBar->visible = (0.0 < scrollbarSize.x && scrollbarSize.x < 1.0); | ||||
verticalScrollBar->visible = (0.0 < scrollbarSize.y && scrollbarSize.y < 1.0); | |||||
horizontalScrollBar->offset = scrollbarOffset.x; | |||||
verticalScrollBar->offset = scrollbarOffset.y; | verticalScrollBar->offset = scrollbarOffset.y; | ||||
horizontalScrollBar->size = scrollbarSize.x; | |||||
verticalScrollBar->size = scrollbarSize.y; | verticalScrollBar->size = scrollbarSize.y; | ||||
verticalScrollBar->visible = (0.0 < scrollbarSize.y && scrollbarSize.y < 1.0); | |||||
Widget::step(); | |||||
// Resize scroll bars | |||||
Vec inner = Vec(box.size.x - verticalScrollBar->box.size.x, box.size.y - horizontalScrollBar->box.size.y); | |||||
horizontalScrollBar->box.pos.y = inner.y; | |||||
verticalScrollBar->box.pos.x = inner.x; | |||||
horizontalScrollBar->box.size.x = verticalScrollBar->visible ? inner.x : box.size.x; | |||||
verticalScrollBar->box.size.y = horizontalScrollBar->visible ? inner.y : box.size.y; | |||||
} | } | ||||
void ScrollWidget::onMouseMove(EventMouseMove &e) { | void ScrollWidget::onMouseMove(EventMouseMove &e) { | ||||
@@ -90,5 +140,9 @@ void ScrollWidget::onScroll(EventScroll &e) { | |||||
e.consumed = true; | e.consumed = true; | ||||
} | } | ||||
void ScrollWidget::onHoverKey(EventHoverKey &e) { | |||||
Widget::onHoverKey(e); | |||||
} | |||||
} // namespace rack | } // namespace rack |
@@ -19,6 +19,8 @@ void TextField::draw(NVGcontext *vg) { | |||||
else | else | ||||
state = BND_DEFAULT; | state = BND_DEFAULT; | ||||
int begin = min(cursor, selection); | |||||
int end = max(cursor, selection); | |||||
bndTextField(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str(), begin, end); | bndTextField(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str(), begin, end); | ||||
// Draw placeholder text | // Draw placeholder text | ||||
if (text.empty() && state != BND_ACTIVE) { | if (text.empty() && state != BND_ACTIVE) { | ||||
@@ -29,25 +31,24 @@ void TextField::draw(NVGcontext *vg) { | |||||
} | } | ||||
void TextField::onMouseDown(EventMouseDown &e) { | void TextField::onMouseDown(EventMouseDown &e) { | ||||
dragPos = getTextPosition(e.pos); | |||||
begin = end = dragPos; | |||||
debug("%d", this == gFocusedWidget); | |||||
if (e.button == 0) { | |||||
cursor = selection = getTextPosition(e.pos); | |||||
} | |||||
OpaqueWidget::onMouseDown(e); | OpaqueWidget::onMouseDown(e); | ||||
} | } | ||||
void TextField::onMouseMove(EventMouseMove &e) { | void TextField::onMouseMove(EventMouseMove &e) { | ||||
if (this == gDraggedWidget) { | if (this == gDraggedWidget) { | ||||
int pos = getTextPosition(e.pos); | int pos = getTextPosition(e.pos); | ||||
if (pos != dragPos) { | |||||
begin = min(dragPos, pos); | |||||
end = max(dragPos, pos); | |||||
if (pos != selection) { | |||||
cursor = pos; | |||||
} | } | ||||
} | } | ||||
OpaqueWidget::onMouseMove(e); | OpaqueWidget::onMouseMove(e); | ||||
} | } | ||||
void TextField::onFocus(EventFocus &e) { | void TextField::onFocus(EventFocus &e) { | ||||
begin = 0; | |||||
end = text.size(); | |||||
e.consumed = true; | e.consumed = true; | ||||
} | } | ||||
@@ -61,85 +62,101 @@ void TextField::onText(EventText &e) { | |||||
void TextField::onKey(EventKey &e) { | void TextField::onKey(EventKey &e) { | ||||
switch (e.key) { | switch (e.key) { | ||||
case GLFW_KEY_BACKSPACE: | |||||
if (begin < end) { | |||||
text.erase(begin, end - begin); | |||||
onTextChange(); | |||||
} | |||||
else { | |||||
begin--; | |||||
if (begin >= 0) { | |||||
text.erase(begin, 1); | |||||
case GLFW_KEY_BACKSPACE: { | |||||
if (cursor == selection) { | |||||
cursor--; | |||||
if (cursor >= 0) { | |||||
text.erase(cursor, 1); | |||||
onTextChange(); | onTextChange(); | ||||
} | } | ||||
selection = cursor; | |||||
} | } | ||||
end = begin; | |||||
break; | |||||
case GLFW_KEY_DELETE: | |||||
if (begin < end) { | |||||
text.erase(begin, end - begin); | |||||
else { | |||||
int begin = min(cursor, selection); | |||||
text.erase(begin, std::abs(selection - cursor)); | |||||
onTextChange(); | |||||
cursor = selection = begin; | |||||
} | |||||
} break; | |||||
case GLFW_KEY_DELETE: { | |||||
if (cursor == selection) { | |||||
text.erase(cursor, 1); | |||||
onTextChange(); | onTextChange(); | ||||
} | } | ||||
else { | else { | ||||
text.erase(begin, 1); | |||||
int begin = min(cursor, selection); | |||||
text.erase(begin, std::abs(selection - cursor)); | |||||
onTextChange(); | onTextChange(); | ||||
cursor = selection = begin; | |||||
} | } | ||||
end = begin; | |||||
break; | |||||
case GLFW_KEY_LEFT: | |||||
if (begin < end) { | |||||
} break; | |||||
case GLFW_KEY_LEFT: { | |||||
if (windowIsModPressed()) { | |||||
while (--cursor > 0) { | |||||
if (text[cursor] == ' ') | |||||
break; | |||||
} | |||||
} | } | ||||
else { | else { | ||||
begin--; | |||||
cursor--; | |||||
} | } | ||||
end = begin; | |||||
break; | |||||
case GLFW_KEY_RIGHT: | |||||
if (begin < end) { | |||||
begin = end; | |||||
if (!windowIsShiftPressed()) { | |||||
selection = cursor; | |||||
} | |||||
} break; | |||||
case GLFW_KEY_RIGHT: { | |||||
if (windowIsModPressed()) { | |||||
while (++cursor < (int) text.size()) { | |||||
if (text[cursor] == ' ') | |||||
break; | |||||
} | |||||
} | } | ||||
else { | else { | ||||
begin++; | |||||
} | |||||
end = begin; | |||||
break; | |||||
case GLFW_KEY_HOME: | |||||
end = begin = 0; | |||||
break; | |||||
case GLFW_KEY_END: | |||||
end = begin = text.size(); | |||||
break; | |||||
case GLFW_KEY_V: | |||||
cursor++; | |||||
} | |||||
if (!windowIsShiftPressed()) { | |||||
selection = cursor; | |||||
} | |||||
} break; | |||||
case GLFW_KEY_HOME: { | |||||
selection = cursor = 0; | |||||
} break; | |||||
case GLFW_KEY_END: { | |||||
selection = cursor = text.size(); | |||||
} break; | |||||
case GLFW_KEY_V: { | |||||
if (windowIsModPressed()) { | if (windowIsModPressed()) { | ||||
const char *newText = glfwGetClipboardString(gWindow); | const char *newText = glfwGetClipboardString(gWindow); | ||||
if (newText) | if (newText) | ||||
insertText(newText); | insertText(newText); | ||||
} | } | ||||
break; | |||||
case GLFW_KEY_X: | |||||
} break; | |||||
case GLFW_KEY_X: { | |||||
if (windowIsModPressed()) { | if (windowIsModPressed()) { | ||||
if (begin < end) { | |||||
std::string selectedText = text.substr(begin, end - begin); | |||||
if (cursor != selection) { | |||||
int begin = min(cursor, selection); | |||||
std::string selectedText = text.substr(begin, std::abs(selection - cursor)); | |||||
glfwSetClipboardString(gWindow, selectedText.c_str()); | glfwSetClipboardString(gWindow, selectedText.c_str()); | ||||
insertText(""); | insertText(""); | ||||
} | } | ||||
} | } | ||||
break; | |||||
case GLFW_KEY_C: | |||||
} break; | |||||
case GLFW_KEY_C: { | |||||
if (windowIsModPressed()) { | if (windowIsModPressed()) { | ||||
if (begin < end) { | |||||
std::string selectedText = text.substr(begin, end - begin); | |||||
if (cursor != selection) { | |||||
int begin = min(cursor, selection); | |||||
std::string selectedText = text.substr(begin, std::abs(selection - cursor)); | |||||
glfwSetClipboardString(gWindow, selectedText.c_str()); | glfwSetClipboardString(gWindow, selectedText.c_str()); | ||||
} | } | ||||
} | } | ||||
break; | |||||
case GLFW_KEY_A: | |||||
} break; | |||||
case GLFW_KEY_A: { | |||||
if (windowIsModPressed()) { | if (windowIsModPressed()) { | ||||
begin = 0; | |||||
end = text.size(); | |||||
selection = 0; | |||||
cursor = text.size(); | |||||
} | } | ||||
break; | |||||
case GLFW_KEY_ENTER: | |||||
} break; | |||||
case GLFW_KEY_ENTER: { | |||||
if (multiline) { | if (multiline) { | ||||
insertText("\n"); | insertText("\n"); | ||||
} | } | ||||
@@ -147,20 +164,29 @@ void TextField::onKey(EventKey &e) { | |||||
EventAction e; | EventAction e; | ||||
onAction(e); | onAction(e); | ||||
} | } | ||||
break; | |||||
} break; | |||||
} | } | ||||
begin = clamp(begin, 0, text.size()); | |||||
end = clamp(end, 0, text.size()); | |||||
cursor = clamp(cursor, 0, text.size()); | |||||
selection = clamp(selection, 0, text.size()); | |||||
e.consumed = true; | e.consumed = true; | ||||
} | } | ||||
void TextField::insertText(std::string newText) { | |||||
if (begin < end) | |||||
text.erase(begin, end - begin); | |||||
text.insert(begin, newText); | |||||
begin += newText.size(); | |||||
end = begin; | |||||
void TextField::insertText(std::string text) { | |||||
if (cursor != selection) { | |||||
int begin = min(cursor, selection); | |||||
this->text.erase(begin, std::abs(selection - cursor)); | |||||
cursor = selection = begin; | |||||
} | |||||
this->text.insert(cursor, text); | |||||
cursor += text.size(); | |||||
selection = cursor; | |||||
onTextChange(); | |||||
} | |||||
void TextField::setText(std::string text) { | |||||
this->text = text; | |||||
selection = cursor = text.size(); | |||||
onTextChange(); | onTextChange(); | ||||
} | } | ||||
@@ -4,12 +4,12 @@ | |||||
namespace rack { | namespace rack { | ||||
void Window::draw(NVGcontext *vg) { | |||||
void WindowWidget::draw(NVGcontext *vg) { | |||||
bndNodeBackground(vg, 0.0, 0.0, box.size.x, box.size.y, BND_DEFAULT, -1, title.c_str(), bndGetTheme()->backgroundColor); | bndNodeBackground(vg, 0.0, 0.0, box.size.x, box.size.y, BND_DEFAULT, -1, title.c_str(), bndGetTheme()->backgroundColor); | ||||
Widget::draw(vg); | Widget::draw(vg); | ||||
} | } | ||||
void Window::onDragMove(EventDragMove &e) { | |||||
void WindowWidget::onDragMove(EventDragMove &e) { | |||||
box.pos = box.pos.plus(e.mouseRel); | box.pos = box.pos.plus(e.mouseRel); | ||||
} | } | ||||
@@ -0,0 +1,39 @@ | |||||
#include "ui.hpp" | |||||
namespace rack { | |||||
void SequentialLayout::step() { | |||||
Widget::step(); | |||||
float offset = 0.0; | |||||
for (Widget *child : children) { | |||||
if (!child->visible) | |||||
continue; | |||||
// Set position | |||||
(orientation == HORIZONTAL_ORIENTATION ? child->box.pos.x : child->box.pos.y) = offset; | |||||
// Increment by size | |||||
offset += (orientation == HORIZONTAL_ORIENTATION ? child->box.size.x : child->box.size.y); | |||||
offset += spacing; | |||||
} | |||||
// We're done if left aligned | |||||
if (alignment == LEFT_ALIGNMENT) | |||||
return; | |||||
// Adjust positions based on width of the layout itself | |||||
offset -= spacing; | |||||
if (alignment == RIGHT_ALIGNMENT) | |||||
offset -= (orientation == HORIZONTAL_ORIENTATION ? box.size.x : box.size.y); | |||||
else if (alignment == CENTER_ALIGNMENT) | |||||
offset -= (orientation == HORIZONTAL_ORIENTATION ? box.size.x : box.size.y) / 2.0; | |||||
for (Widget *child : children) { | |||||
if (!child->visible) | |||||
continue; | |||||
(orientation == HORIZONTAL_ORIENTATION ? child->box.pos.x : child->box.pos.y) += offset; | |||||
} | |||||
} | |||||
} // namespace rack |
@@ -1,11 +0,0 @@ | |||||
#include <stdarg.h> | |||||
#include <string.h> | |||||
#include <random> | |||||
#include <algorithm> | |||||
#include <libgen.h> // for dirname and basename | |||||
#include <sys/time.h> | |||||
#if ARCH_WIN | |||||
#include <windows.h> | |||||
#include <shellapi.h> | |||||
#endif |
@@ -460,8 +460,9 @@ void windowRun() { | |||||
int windowWidth, windowHeight; | int windowWidth, windowHeight; | ||||
glfwGetWindowSize(gWindow, &windowWidth, &windowHeight); | glfwGetWindowSize(gWindow, &windowWidth, &windowHeight); | ||||
gWindowRatio = (float)width / windowWidth; | gWindowRatio = (float)width / windowWidth; | ||||
debug("%d %d %f %f", windowWidth, windowHeight, gWindowRatio, gPixelRatio); | |||||
gScene->box.size = Vec(width, height).div(gPixelRatio / gWindowRatio); | |||||
gScene->box.size = Vec(windowWidth, windowHeight); | |||||
// Step scene | // Step scene | ||||
gScene->step(); | gScene->step(); | ||||