| @@ -11,7 +11,7 @@ ifeq ($(ARCH), lin) | |||
| SOURCES += ext/noc/noc_file_dialog.c | |||
| CFLAGS += -DNOC_FILE_DIALOG_GTK $(shell pkg-config --cflags gtk+-2.0) | |||
| LDFLAGS += -rdynamic \ | |||
| -lpthread -lGL -lGLEW -lglfw -ldl -ljansson -lportaudio -lportmidi -lsamplerate \ | |||
| -lpthread -lGL -lGLEW -lglfw -ldl -ljansson -lportaudio -lportmidi -lsamplerate -lcurl -lzip \ | |||
| $(shell pkg-config --libs gtk+-2.0) | |||
| TARGET = Rack | |||
| endif | |||
| @@ -20,7 +20,7 @@ ifeq ($(ARCH), mac) | |||
| SOURCES += ext/noc/noc_file_dialog.m | |||
| CFLAGS += -DNOC_FILE_DIALOG_OSX | |||
| CXXFLAGS += -DAPPLE -stdlib=libc++ -I$(HOME)/local/include | |||
| LDFLAGS += -stdlib=libc++ -L$(HOME)/local/lib -lpthread -lglew -lglfw3 -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo -ldl -ljansson -lportaudio -lportmidi -lsamplerate | |||
| LDFLAGS += -stdlib=libc++ -L$(HOME)/local/lib -lpthread -lglew -lglfw3 -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo -ldl -ljansson -lportaudio -lportmidi -lsamplerate -lcurl -lzip | |||
| TARGET = Rack | |||
| endif | |||
| @@ -31,7 +31,7 @@ CXXFLAGS += -DGLEW_STATIC \ | |||
| -I$(HOME)/pkg/portaudio-r1891-build/include | |||
| LDFLAGS += \ | |||
| -Wl,-Bstatic,--whole-archive \ | |||
| -lglfw3 -lgdi32 -lglew32 -ljansson -lsamplerate \ | |||
| -lglfw3 -lgdi32 -lglew32 -ljansson -lsamplerate -lcurl -lzip \ | |||
| -Wl,-Bdynamic,--no-whole-archive \ | |||
| -lpthread -lopengl32 -lcomdlg32 -lole32 \ | |||
| -lportmidi \ | |||
| @@ -24,6 +24,6 @@ If the build breaks because you think I've missed a step, feel free to post an i | |||
| ## License | |||
| Rack source code by Andrew Belt: BSD-3-Clause | |||
| Rack source code by [Andrew Belt](https://andrewbelt.name/): [BSD-3-Clause](LICENSE.txt) | |||
| Component Library graphics by [Grayscale](http://grayscale.info/): [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) | |||
| @@ -234,6 +234,14 @@ struct Toolbar : OpaqueWidget { | |||
| void draw(NVGcontext *vg); | |||
| }; | |||
| struct PluginManagerWidget : Widget { | |||
| Widget *loginWidget; | |||
| Widget *manageWidget; | |||
| Widget *downloadWidget; | |||
| PluginManagerWidget(); | |||
| void step(); | |||
| }; | |||
| struct RackScene : Scene { | |||
| Toolbar *toolbar; | |||
| ScrollWidget *scrollWidget; | |||
| @@ -37,6 +37,16 @@ extern std::list<Plugin*> gPlugins; | |||
| void pluginInit(); | |||
| void pluginDestroy(); | |||
| void pluginOpenBrowser(std::string url); | |||
| void pluginLogIn(std::string email, std::string password); | |||
| void pluginLogOut(); | |||
| void pluginRefresh(); | |||
| void pluginCancelDownload(); | |||
| bool pluginIsLoggedIn(); | |||
| bool pluginIsDownloading(); | |||
| float pluginGetDownloadProgress(); | |||
| std::string pluginGetDownloadName(); | |||
| } // namespace rack | |||
| @@ -52,6 +52,7 @@ struct Widget { | |||
| Rect box = Rect(Vec(), Vec(INFINITY, INFINITY)); | |||
| Widget *parent = NULL; | |||
| std::list<Widget*> children; | |||
| bool visible = true; | |||
| virtual ~Widget(); | |||
| @@ -219,10 +220,10 @@ struct QuantityWidget : virtual Widget { | |||
| std::string label; | |||
| /** Include a space character if you want a space after the number, e.g. " Hz" */ | |||
| std::string unit; | |||
| /** The digit place to round for displaying values. | |||
| A precision of -2 will display as "1.00" for example. | |||
| /** The decimal place to round for displaying values. | |||
| A precision of 2 will display as "1.00" for example. | |||
| */ | |||
| int precision = -2; | |||
| int precision = 2; | |||
| QuantityWidget(); | |||
| void setValue(float value); | |||
| @@ -357,6 +358,7 @@ struct ScrollWidget : OpaqueWidget { | |||
| struct TextField : OpaqueWidget { | |||
| std::string text; | |||
| std::string placeholder; | |||
| int begin = 0; | |||
| int end = 0; | |||
| @@ -374,6 +376,13 @@ struct PasswordField : TextField { | |||
| void draw(NVGcontext *vg); | |||
| }; | |||
| struct ProgressBar : TransparentWidget, QuantityWidget { | |||
| ProgressBar() { | |||
| box.size.y = BND_WIDGET_HEIGHT; | |||
| } | |||
| void draw(NVGcontext *vg); | |||
| }; | |||
| struct Tooltip : Widget { | |||
| void step(); | |||
| void draw(NVGcontext *vg); | |||
| @@ -0,0 +1,166 @@ | |||
| #include <thread> | |||
| #include "app.hpp" | |||
| #include "plugin.hpp" | |||
| namespace rack { | |||
| PluginManagerWidget::PluginManagerWidget() { | |||
| box.size.y = BND_WIDGET_HEIGHT; | |||
| float margin = 5; | |||
| { | |||
| loginWidget = new Widget(); | |||
| Vec pos = Vec(0, 0); | |||
| struct RegisterButton : Button { | |||
| void onAction() { | |||
| std::thread t(pluginOpenBrowser, "http://vcvrack.com/"); | |||
| t.detach(); | |||
| } | |||
| }; | |||
| Button *registerButton = new RegisterButton(); | |||
| registerButton->box.pos = pos; | |||
| registerButton->box.size.x = 75; | |||
| registerButton->text = "Register"; | |||
| loginWidget->addChild(registerButton); | |||
| pos.x += registerButton->box.size.x; | |||
| pos.x += margin; | |||
| TextField *emailField = new TextField(); | |||
| emailField->box.pos = pos; | |||
| emailField->box.size.x = 175; | |||
| emailField->placeholder = "Email"; | |||
| loginWidget->addChild(emailField); | |||
| pos.x += emailField->box.size.x; | |||
| pos.x += margin; | |||
| PasswordField *passwordField = new PasswordField(); | |||
| passwordField->box.pos = pos; | |||
| passwordField->box.size.x = 175; | |||
| passwordField->placeholder = "Password"; | |||
| loginWidget->addChild(passwordField); | |||
| pos.x += passwordField->box.size.x; | |||
| struct LogInButton : Button { | |||
| TextField *emailField; | |||
| TextField *passwordField; | |||
| void onAction() { | |||
| std::thread t(pluginLogIn, emailField->text, passwordField->text); | |||
| t.detach(); | |||
| passwordField->text = ""; | |||
| } | |||
| }; | |||
| pos.x += margin; | |||
| LogInButton *logInButton = new LogInButton(); | |||
| logInButton->box.pos = pos; | |||
| logInButton->box.size.x = 100; | |||
| logInButton->text = "Log in"; | |||
| logInButton->emailField = emailField; | |||
| logInButton->passwordField = passwordField; | |||
| loginWidget->addChild(logInButton); | |||
| addChild(loginWidget); | |||
| } | |||
| { | |||
| manageWidget = new Widget(); | |||
| Vec pos = Vec(0, 0); | |||
| struct ManageButton : Button { | |||
| void onAction() { | |||
| std::thread t(pluginOpenBrowser, "http://vcvrack.com/"); | |||
| t.detach(); | |||
| } | |||
| }; | |||
| Button *manageButton = new ManageButton(); | |||
| manageButton->box.pos = pos; | |||
| manageButton->box.size.x = 125; | |||
| manageButton->text = "Manage plugins"; | |||
| manageWidget->addChild(manageButton); | |||
| pos.x += manageButton->box.size.x; | |||
| struct RefreshButton : Button { | |||
| void onAction() { | |||
| std::thread t(pluginRefresh); | |||
| t.detach(); | |||
| } | |||
| }; | |||
| pos.x += margin; | |||
| Button *refreshButton = new RefreshButton(); | |||
| refreshButton->box.pos = pos; | |||
| refreshButton->box.size.x = 125; | |||
| refreshButton->text = "Refresh plugins"; | |||
| manageWidget->addChild(refreshButton); | |||
| pos.x += refreshButton->box.size.x; | |||
| struct LogOutButton : Button { | |||
| void onAction() { | |||
| pluginLogOut(); | |||
| } | |||
| }; | |||
| pos.x += margin; | |||
| Button *logOutButton = new LogOutButton(); | |||
| logOutButton->box.pos = pos; | |||
| logOutButton->box.size.x = 100; | |||
| logOutButton->text = "Log out"; | |||
| manageWidget->addChild(logOutButton); | |||
| addChild(manageWidget); | |||
| } | |||
| { | |||
| downloadWidget = new Widget(); | |||
| Vec pos = Vec(0, 0); | |||
| struct DownloadProgressBar : ProgressBar { | |||
| void step() { | |||
| label = "Downloading"; | |||
| std::string name = pluginGetDownloadName(); | |||
| if (name != "") | |||
| label += " " + name; | |||
| setValue(100.0 * pluginGetDownloadProgress()); | |||
| } | |||
| }; | |||
| ProgressBar *downloadProgress = new DownloadProgressBar(); | |||
| downloadProgress->box.pos = pos; | |||
| downloadProgress->box.size.x = 300; | |||
| downloadProgress->setLimits(0, 100); | |||
| downloadProgress->unit = "%"; | |||
| downloadWidget->addChild(downloadProgress); | |||
| pos.x += downloadProgress->box.size.x; | |||
| // struct CancelButton : Button { | |||
| // void onAction() { | |||
| // pluginCancelDownload(); | |||
| // } | |||
| // }; | |||
| // pos.x += margin; | |||
| // Button *logOutButton = new CancelButton(); | |||
| // logOutButton->box.pos = pos; | |||
| // logOutButton->box.size.x = 100; | |||
| // logOutButton->text = "Cancel"; | |||
| // downloadWidget->addChild(logOutButton); | |||
| addChild(downloadWidget); | |||
| } | |||
| } | |||
| void PluginManagerWidget::step() { | |||
| loginWidget->visible = false; | |||
| manageWidget->visible = false; | |||
| downloadWidget->visible = false; | |||
| if (pluginIsDownloading()) | |||
| downloadWidget->visible = true; | |||
| else if (pluginIsLoggedIn()) | |||
| manageWidget->visible = true; | |||
| else | |||
| loginWidget->visible = true; | |||
| Widget::step(); | |||
| } | |||
| } // namespace rack | |||
| @@ -96,15 +96,7 @@ struct SampleRateChoice : ChoiceButton { | |||
| Toolbar::Toolbar() { | |||
| float margin = 5; | |||
| box.size.y = BND_WIDGET_HEIGHT + 2*margin; | |||
| float xPos = margin; | |||
| { | |||
| Label *label = new Label(); | |||
| label->box.pos = Vec(xPos, margin); | |||
| label->text = gApplicationVersion; | |||
| addChild(label); | |||
| xPos += 100; | |||
| } | |||
| float xPos = 0; | |||
| xPos += margin; | |||
| { | |||
| @@ -163,6 +155,14 @@ Toolbar::Toolbar() { | |||
| addChild(cpuUsageButton); | |||
| xPos += cpuUsageButton->box.size.x; | |||
| } | |||
| xPos += margin; | |||
| { | |||
| Widget *pluginManager = new PluginManagerWidget(); | |||
| pluginManager->box.pos = Vec(xPos, margin); | |||
| addChild(pluginManager); | |||
| xPos += pluginManager->box.size.x; | |||
| } | |||
| } | |||
| void Toolbar::draw(NVGcontext *vg) { | |||
| @@ -195,7 +195,8 @@ void guiInit() { | |||
| glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); | |||
| glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); | |||
| glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); | |||
| window = glfwCreateWindow(1000, 750, gApplicationName.c_str(), NULL, NULL); | |||
| std::string title = gApplicationName + " " + gApplicationVersion; | |||
| window = glfwCreateWindow(1000, 750, title.c_str(), NULL, NULL); | |||
| assert(window); | |||
| glfwMakeContextCurrent(window); | |||
| @@ -27,10 +27,10 @@ int main() { | |||
| } | |||
| #endif | |||
| pluginInit(); | |||
| engineInit(); | |||
| guiInit(); | |||
| sceneInit(); | |||
| pluginInit(); | |||
| gRackWidget->loadPatch("autosave.json"); | |||
| engineStart(); | |||
| @@ -1,5 +1,12 @@ | |||
| #include <stdio.h> | |||
| #include <assert.h> | |||
| #include <string.h> | |||
| #include <unistd.h> | |||
| #include <sys/types.h> | |||
| #include <sys/stat.h> | |||
| #include <fcntl.h> | |||
| #if defined(WINDOWS) | |||
| #include <windows.h> | |||
| #elif defined(LINUX) || defined(APPLE) | |||
| @@ -7,6 +14,10 @@ | |||
| #include <glob.h> | |||
| #endif | |||
| #include <curl/curl.h> | |||
| #include <zip.h> | |||
| #include <jansson.h> | |||
| #include "plugin.hpp" | |||
| @@ -14,23 +25,36 @@ namespace rack { | |||
| std::list<Plugin*> gPlugins; | |||
| static | |||
| int loadPlugin(const char *path) { | |||
| static const std::string apiUrl = "http://localhost:8081"; | |||
| static std::string token; | |||
| static bool isDownloading = false; | |||
| static float downloadProgress = 0.0; | |||
| static std::string downloadName; | |||
| Plugin::~Plugin() { | |||
| for (Model *model : models) { | |||
| delete model; | |||
| } | |||
| } | |||
| static int loadPlugin(const char *path) { | |||
| // Load dynamic/shared library | |||
| #if defined(WINDOWS) | |||
| HINSTANCE handle = LoadLibrary(path); | |||
| if (!handle) { | |||
| fprintf(stderr, "Failed to load library %s\n", path); | |||
| return -1; | |||
| } | |||
| if (!handle) { | |||
| fprintf(stderr, "Failed to load library %s\n", path); | |||
| return -1; | |||
| } | |||
| #elif defined(LINUX) || defined(APPLE) | |||
| char ppath[1024]; | |||
| snprintf(ppath, sizeof(ppath), "./%s", path); | |||
| void *handle = dlopen(ppath, RTLD_NOW | RTLD_GLOBAL); | |||
| if (!handle) { | |||
| fprintf(stderr, "Failed to load library %s: %s\n", path, dlerror()); | |||
| return -1; | |||
| } | |||
| if (!handle) { | |||
| fprintf(stderr, "Failed to load library %s: %s\n", path, dlerror()); | |||
| return -1; | |||
| } | |||
| #endif | |||
| // Call plugin init() function | |||
| @@ -58,6 +82,8 @@ int loadPlugin(const char *path) { | |||
| } | |||
| void pluginInit() { | |||
| curl_global_init(CURL_GLOBAL_ALL); | |||
| // Load core | |||
| // This function is defined in core.cpp | |||
| Plugin *corePlugin = init(); | |||
| @@ -95,18 +121,249 @@ void pluginInit() { | |||
| void pluginDestroy() { | |||
| for (Plugin *plugin : gPlugins) { | |||
| // TODO unload plugin with `dlclose` or `FreeLibrary` | |||
| // TODO free shared library handle with `dlclose` or `FreeLibrary` | |||
| delete plugin; | |||
| } | |||
| gPlugins.clear(); | |||
| curl_global_cleanup(); | |||
| } | |||
| //////////////////// | |||
| // CURL and libzip helpers | |||
| //////////////////// | |||
| Plugin::~Plugin() { | |||
| for (Model *model : models) { | |||
| delete model; | |||
| static size_t write_file_callback(void *data, size_t size, size_t nmemb, void *p) { | |||
| int fd = *((int*)p); | |||
| ssize_t len = write(fd, data, size*nmemb); | |||
| return len; | |||
| } | |||
| static int progress_callback(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow) { | |||
| if (dltotal == 0.0) | |||
| return 0; | |||
| float progress = dlnow / dltotal; | |||
| downloadProgress = progress; | |||
| return 0; | |||
| } | |||
| static CURLcode download_file(int fd, const char *url) { | |||
| CURL *curl = curl_easy_init(); | |||
| curl_easy_setopt(curl, CURLOPT_URL, url); | |||
| curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); | |||
| curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); | |||
| curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file_callback); | |||
| curl_easy_setopt(curl, CURLOPT_WRITEDATA, &fd); | |||
| curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progress_callback); | |||
| curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, NULL); | |||
| CURLcode res = curl_easy_perform(curl); | |||
| curl_easy_cleanup(curl); | |||
| return res; | |||
| } | |||
| static size_t write_string_callback(void *data, size_t size, size_t nmemb, void *p) { | |||
| std::string &text = *((std::string*)p); | |||
| char *dataStr = (char*) data; | |||
| size_t len = size * nmemb; | |||
| text.append(dataStr, len); | |||
| return len; | |||
| } | |||
| static void extract_zip(int zipfd, int dirfd) { | |||
| int err = 0; | |||
| zip_t *za = zip_fdopen(zipfd, 0, &err); | |||
| if (!za) return; | |||
| if (err) goto cleanup; | |||
| for (int i = 0; i < zip_get_num_entries(za, 0); i++) { | |||
| zip_stat_t zs; | |||
| err = zip_stat_index(za, i, 0, &zs); | |||
| if (err) goto cleanup; | |||
| int nameLen = strlen(zs.name); | |||
| if (zs.name[nameLen - 1] == '/') { | |||
| err = mkdirat(dirfd, zs.name, 0755); | |||
| if (err) goto cleanup; | |||
| } | |||
| else { | |||
| zip_file_t *zf = zip_fopen_index(za, i, 0); | |||
| if (!zf) goto cleanup; | |||
| int out = openat(dirfd, zs.name, O_RDWR | O_TRUNC | O_CREAT, 0644); | |||
| assert(out != -1); | |||
| while (1) { | |||
| char buffer[4096]; | |||
| int len = zip_fread(zf, buffer, sizeof(buffer)); | |||
| if (len <= 0) | |||
| break; | |||
| write(out, buffer, len); | |||
| } | |||
| err = zip_fclose(zf); | |||
| assert(!err); | |||
| close(out); | |||
| } | |||
| } | |||
| cleanup: | |||
| zip_close(za); | |||
| } | |||
| //////////////////// | |||
| // plugin manager | |||
| //////////////////// | |||
| void pluginOpenBrowser(std::string url) { | |||
| // shell injection is possible, so make sure the URL is trusted | |||
| #if defined(LINUX) | |||
| std::string command = "xdg-open " + url; | |||
| system(command.c_str()); | |||
| #endif | |||
| #if defined(APPLE) | |||
| std::string command = "open " + url; | |||
| system(command.c_str()); | |||
| #endif | |||
| #if defined(WINDOWS) | |||
| ShellExecute(NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL); | |||
| #endif | |||
| } | |||
| void pluginLogIn(std::string email, std::string password) { | |||
| CURL *curl = curl_easy_init(); | |||
| assert(curl); | |||
| std::string postFields = "email=" + email + "&password=" + password; | |||
| std::string url = apiUrl + "/token"; | |||
| std::string resText; | |||
| curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); | |||
| curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_string_callback); | |||
| curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resText); | |||
| curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields.c_str()); | |||
| curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, postFields.size()); | |||
| CURLcode res = curl_easy_perform(curl); | |||
| curl_easy_cleanup(curl); | |||
| if (res == CURLE_OK) { | |||
| // Parse JSON response | |||
| json_error_t error; | |||
| json_t *root = json_loads(resText.c_str(), 0, &error); | |||
| if (root) { | |||
| json_t *tokenJ = json_object_get(root, "token"); | |||
| if (tokenJ) { | |||
| // Set the token, which logs the user in | |||
| token = json_string_value(tokenJ); | |||
| } | |||
| json_decref(root); | |||
| } | |||
| } | |||
| } | |||
| void pluginLogOut() { | |||
| token = ""; | |||
| } | |||
| static void pluginRefreshPlugin(json_t *pluginJ) { | |||
| json_t *slugJ = json_object_get(pluginJ, "slug"); | |||
| if (!slugJ) return; | |||
| std::string slug = json_string_value(slugJ); | |||
| json_t *nameJ = json_object_get(pluginJ, "name"); | |||
| if (!nameJ) return; | |||
| std::string name = json_string_value(nameJ); | |||
| json_t *urlJ = json_object_get(pluginJ, "download"); | |||
| if (!urlJ) return; | |||
| std::string url = json_string_value(urlJ); | |||
| // Find slug in plugins list | |||
| for (Plugin *p : gPlugins) { | |||
| if (p->slug == slug) { | |||
| return; | |||
| } | |||
| } | |||
| // If plugin is not loaded, download the zip file to /plugins | |||
| fprintf(stderr, "Downloading %s from %s\n", name.c_str(), url.c_str()); | |||
| downloadName = name; | |||
| downloadProgress = 0.0; | |||
| std::string filename = slug + ".zip"; | |||
| int dir = open("plugins", O_RDONLY | O_DIRECTORY); | |||
| int zip = openat(dir, filename.c_str(), O_RDWR | O_TRUNC | O_CREAT, 0644); | |||
| // Download zip | |||
| download_file(zip, url.c_str()); | |||
| // Unzip file | |||
| lseek(zip, 0, SEEK_SET); | |||
| extract_zip(zip, dir); | |||
| // Close files | |||
| close(zip); | |||
| close(dir); | |||
| downloadName = ""; | |||
| } | |||
| void pluginRefresh() { | |||
| if (token.empty()) | |||
| return; | |||
| isDownloading = true; | |||
| downloadProgress = 0.0; | |||
| downloadName = ""; | |||
| // Get plugin list from /plugin | |||
| CURL *curl = curl_easy_init(); | |||
| assert(curl); | |||
| std::string url = apiUrl + "/plugins?token=" + token; | |||
| std::string resText; | |||
| curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); | |||
| curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_string_callback); | |||
| curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resText); | |||
| CURLcode res = curl_easy_perform(curl); | |||
| curl_easy_cleanup(curl); | |||
| if (res == CURLE_OK) { | |||
| // Parse JSON response | |||
| json_error_t error; | |||
| json_t *root = json_loads(resText.c_str(), 0, &error); | |||
| if (root) { | |||
| json_t *pluginsJ = json_object_get(root, "plugins"); | |||
| if (pluginsJ) { | |||
| // Iterate through each plugin object | |||
| size_t index; | |||
| json_t *pluginJ; | |||
| json_array_foreach(pluginsJ, index, pluginJ) { | |||
| pluginRefreshPlugin(pluginJ); | |||
| } | |||
| } | |||
| json_decref(root); | |||
| } | |||
| } | |||
| isDownloading = false; | |||
| } | |||
| void pluginCancelDownload() { | |||
| // TODO | |||
| } | |||
| bool pluginIsLoggedIn() { | |||
| return token != ""; | |||
| } | |||
| bool pluginIsDownloading() { | |||
| return isDownloading; | |||
| } | |||
| float pluginGetDownloadProgress() { | |||
| return downloadProgress; | |||
| } | |||
| std::string pluginGetDownloadName() { | |||
| return downloadName; | |||
| } | |||
| } // namespace rack | |||
| @@ -34,11 +34,10 @@ std::string stringf(const char *format, ...) { | |||
| va_end(args); | |||
| if (size < 0) | |||
| return ""; | |||
| size++; | |||
| std::string s; | |||
| s.resize(size); | |||
| va_start(args, format); | |||
| vsnprintf(&s[0], size, format, args); | |||
| vsnprintf(&s[0], size+1, format, args); | |||
| va_end(args); | |||
| return s; | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| #include "widgets.hpp" | |||
| namespace rack { | |||
| void ProgressBar::draw(NVGcontext *vg) { | |||
| float progress = mapf(value, minValue, maxValue, 0.0, 1.0); | |||
| bndSlider(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_ALL, BND_DEFAULT, progress, getText().c_str(), NULL); | |||
| } | |||
| } // namespace rack | |||
| @@ -25,16 +25,7 @@ void QuantityWidget::setDefaultValue(float defaultValue) { | |||
| std::string QuantityWidget::getText() { | |||
| std::string text = label; | |||
| text += ": "; | |||
| char valueStr[128]; | |||
| if (precision >= 0) { | |||
| float factor = powf(10.0, precision); | |||
| float v = roundf(value / factor) * factor; | |||
| snprintf(valueStr, sizeof(valueStr), "%.0f", v); | |||
| } | |||
| else { | |||
| snprintf(valueStr, sizeof(valueStr), "%.*f", -precision, value); | |||
| } | |||
| text += valueStr; | |||
| text += stringf("%.*f", precision, value); | |||
| text += unit; | |||
| return text; | |||
| } | |||
| @@ -18,6 +18,9 @@ void TextField::draw(NVGcontext *vg) { | |||
| state = BND_DEFAULT; | |||
| bndTextField(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str(), begin, end); | |||
| if (text.empty() && state != BND_ACTIVE) { | |||
| bndIconLabelCaret(vg, 0.0, 0.0, box.size.x, box.size.y, -1, bndGetTheme()->textFieldTheme.itemColor, 13, placeholder.c_str(), bndGetTheme()->textFieldTheme.itemColor, 0, -1); | |||
| } | |||
| } | |||
| Widget *TextField::onMouseDown(Vec pos, int button) { | |||
| @@ -70,6 +70,8 @@ void Widget::step() { | |||
| void Widget::draw(NVGcontext *vg) { | |||
| for (Widget *child : children) { | |||
| if (!child->visible) | |||
| continue; | |||
| nvgSave(vg); | |||
| nvgTranslate(vg, child->box.pos.x, child->box.pos.y); | |||
| child->draw(vg); | |||
| @@ -80,6 +82,8 @@ void Widget::draw(NVGcontext *vg) { | |||
| Widget *Widget::onMouseDown(Vec pos, int button) { | |||
| for (auto it = children.rbegin(); it != children.rend(); it++) { | |||
| Widget *child = *it; | |||
| if (!child->visible) | |||
| continue; | |||
| if (child->box.contains(pos)) { | |||
| Widget *w = child->onMouseDown(pos.minus(child->box.pos), button); | |||
| if (w) | |||
| @@ -92,6 +96,8 @@ Widget *Widget::onMouseDown(Vec pos, int button) { | |||
| Widget *Widget::onMouseUp(Vec pos, int button) { | |||
| for (auto it = children.rbegin(); it != children.rend(); it++) { | |||
| Widget *child = *it; | |||
| if (!child->visible) | |||
| continue; | |||
| if (child->box.contains(pos)) { | |||
| Widget *w = child->onMouseUp(pos.minus(child->box.pos), button); | |||
| if (w) | |||
| @@ -104,6 +110,8 @@ Widget *Widget::onMouseUp(Vec pos, int button) { | |||
| Widget *Widget::onMouseMove(Vec pos, Vec mouseRel) { | |||
| for (auto it = children.rbegin(); it != children.rend(); it++) { | |||
| Widget *child = *it; | |||
| if (!child->visible) | |||
| continue; | |||
| if (child->box.contains(pos)) { | |||
| Widget *w = child->onMouseMove(pos.minus(child->box.pos), mouseRel); | |||
| if (w) | |||
| @@ -116,6 +124,8 @@ Widget *Widget::onMouseMove(Vec pos, Vec mouseRel) { | |||
| Widget *Widget::onScroll(Vec pos, Vec scrollRel) { | |||
| for (auto it = children.rbegin(); it != children.rend(); it++) { | |||
| Widget *child = *it; | |||
| if (!child->visible) | |||
| continue; | |||
| if (child->box.contains(pos)) { | |||
| Widget *w = child->onScroll(pos.minus(child->box.pos), scrollRel); | |||
| if (w) | |||