diff --git a/Makefile b/Makefile index 594186e4..323165b8 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/README.md b/README.md index ebd60f58..cd2367a1 100644 --- a/README.md +++ b/README.md @@ -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/) diff --git a/include/app.hpp b/include/app.hpp index e34b6940..02d99a3b 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -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; diff --git a/include/plugin.hpp b/include/plugin.hpp index cb4c68b1..c3f70b39 100644 --- a/include/plugin.hpp +++ b/include/plugin.hpp @@ -37,6 +37,16 @@ extern std::list 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 diff --git a/include/widgets.hpp b/include/widgets.hpp index 3dc40941..135ff1c4 100644 --- a/include/widgets.hpp +++ b/include/widgets.hpp @@ -52,6 +52,7 @@ struct Widget { Rect box = Rect(Vec(), Vec(INFINITY, INFINITY)); Widget *parent = NULL; std::list 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); diff --git a/src/app/PluginManagerWidget.cpp b/src/app/PluginManagerWidget.cpp new file mode 100644 index 00000000..4e8835b2 --- /dev/null +++ b/src/app/PluginManagerWidget.cpp @@ -0,0 +1,166 @@ +#include +#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 diff --git a/src/app/Toolbar.cpp b/src/app/Toolbar.cpp index 04bc619f..d858d2a5 100644 --- a/src/app/Toolbar.cpp +++ b/src/app/Toolbar.cpp @@ -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) { diff --git a/src/gui.cpp b/src/gui.cpp index 094157f0..d219217d 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -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); diff --git a/src/main.cpp b/src/main.cpp index 159c9b6f..24fa27ab 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,10 +27,10 @@ int main() { } #endif + pluginInit(); engineInit(); guiInit(); sceneInit(); - pluginInit(); gRackWidget->loadPatch("autosave.json"); engineStart(); diff --git a/src/plugin.cpp b/src/plugin.cpp index 1c3db217..7c6003b1 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -1,5 +1,12 @@ #include +#include +#include +#include +#include +#include +#include + #if defined(WINDOWS) #include #elif defined(LINUX) || defined(APPLE) @@ -7,6 +14,10 @@ #include #endif +#include +#include +#include + #include "plugin.hpp" @@ -14,23 +25,36 @@ namespace rack { std::list 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 diff --git a/src/util.cpp b/src/util.cpp index fe7218fa..55eb2977 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -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; } diff --git a/src/widgets/ProgressBar.cpp b/src/widgets/ProgressBar.cpp new file mode 100644 index 00000000..8edb7ee9 --- /dev/null +++ b/src/widgets/ProgressBar.cpp @@ -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 diff --git a/src/widgets/QuantityWidget.cpp b/src/widgets/QuantityWidget.cpp index a2dcf3ad..f1bd8763 100644 --- a/src/widgets/QuantityWidget.cpp +++ b/src/widgets/QuantityWidget.cpp @@ -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; } diff --git a/src/widgets/TextField.cpp b/src/widgets/TextField.cpp index 8215a18c..816dbd3b 100644 --- a/src/widgets/TextField.cpp +++ b/src/widgets/TextField.cpp @@ -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) { diff --git a/src/widgets/Widget.cpp b/src/widgets/Widget.cpp index c075c2b5..126d53a2 100644 --- a/src/widgets/Widget.cpp +++ b/src/widgets/Widget.cpp @@ -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)