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