Browse Source

Add plugin manager for downloading plugins within the application, add libzip and curl dependency

tags/v0.3.0
Andrew Belt 8 years ago
parent
commit
12ff32e3ca
15 changed files with 510 additions and 44 deletions
  1. +3
    -3
      Makefile
  2. +1
    -1
      README.md
  3. +8
    -0
      include/app.hpp
  4. +10
    -0
      include/plugin.hpp
  5. +12
    -3
      include/widgets.hpp
  6. +166
    -0
      src/app/PluginManagerWidget.cpp
  7. +9
    -9
      src/app/Toolbar.cpp
  8. +2
    -1
      src/gui.cpp
  9. +1
    -1
      src/main.cpp
  10. +271
    -14
      src/plugin.cpp
  11. +1
    -2
      src/util.cpp
  12. +12
    -0
      src/widgets/ProgressBar.cpp
  13. +1
    -10
      src/widgets/QuantityWidget.cpp
  14. +3
    -0
      src/widgets/TextField.cpp
  15. +10
    -0
      src/widgets/Widget.cpp

+ 3
- 3
Makefile View File

@@ -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 \


+ 1
- 1
README.md View File

@@ -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/)

+ 8
- 0
include/app.hpp View File

@@ -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;


+ 10
- 0
include/plugin.hpp View File

@@ -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




+ 12
- 3
include/widgets.hpp View File

@@ -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);


+ 166
- 0
src/app/PluginManagerWidget.cpp View File

@@ -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

+ 9
- 9
src/app/Toolbar.cpp View File

@@ -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) {


+ 2
- 1
src/gui.cpp View File

@@ -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);



+ 1
- 1
src/main.cpp View File

@@ -27,10 +27,10 @@ int main() {
}
#endif

pluginInit();
engineInit();
guiInit();
sceneInit();
pluginInit();
gRackWidget->loadPatch("autosave.json");

engineStart();


+ 271
- 14
src/plugin.cpp View File

@@ -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

+ 1
- 2
src/util.cpp View File

@@ -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;
}


+ 12
- 0
src/widgets/ProgressBar.cpp View File

@@ -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

+ 1
- 10
src/widgets/QuantityWidget.cpp View File

@@ -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;
}


+ 3
- 0
src/widgets/TextField.cpp View File

@@ -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) {


+ 10
- 0
src/widgets/Widget.cpp View File

@@ -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)


Loading…
Cancel
Save