#include #include #include #ifdef ARCH_MAC // For CGAssociateMouseAndMouseCursorPosition #include #endif #include "osdialog.h" #include "rack.hpp" #include "keyboard.hpp" #include "gamepad.hpp" #include "event.hpp" #define NANOVG_GL2_IMPLEMENTATION 1 // #define NANOVG_GL3_IMPLEMENTATION 1 // #define NANOVG_GLES2_IMPLEMENTATION 1 // #define NANOVG_GLES3_IMPLEMENTATION 1 #include "nanovg_gl.h" // Hack to get framebuffer objects working on OpenGL 2 (we blindly assume the extension is supported) #define NANOVG_FBO_VALID 1 #include "nanovg_gl_utils.h" #define BLENDISH_IMPLEMENTATION #include "blendish.h" #define NANOSVG_IMPLEMENTATION #define NANOSVG_ALL_COLOR_KEYWORDS #include "nanosvg.h" namespace rack { GLFWwindow *gWindow = NULL; NVGcontext *gVg = NULL; NVGcontext *gFramebufferVg = NULL; std::shared_ptr gGuiFont; float gPixelRatio = 1.0; float gWindowRatio = 1.0; bool gAllowCursorLock = true; int gGuiFrame; Vec gMousePos; std::string lastWindowTitle; static void windowSizeCallback(GLFWwindow* window, int width, int height) { // Do nothing. Window size is reset each frame anyway. } static void mouseButtonCallback(GLFWwindow *window, int button, int action, int mods) { #ifdef ARCH_MAC // Remap Ctrl-left click to right click on Mac if (button == GLFW_MOUSE_BUTTON_LEFT) { if (mods & GLFW_MOD_CONTROL) { button = GLFW_MOUSE_BUTTON_RIGHT; } } #endif event::gContext->handleButton(gMousePos, button, action, mods); } struct MouseButtonArguments { GLFWwindow *window; int button; int action; int mods; }; static std::queue mouseButtonQueue; void mouseButtonStickyPop() { if (!mouseButtonQueue.empty()) { MouseButtonArguments args = mouseButtonQueue.front(); mouseButtonQueue.pop(); mouseButtonCallback(args.window, args.button, args.action, args.mods); } } void mouseButtonStickyCallback(GLFWwindow *window, int button, int action, int mods) { // Defer multiple clicks per frame to future frames MouseButtonArguments args = {window, button, action, mods}; mouseButtonQueue.push(args); } void cursorPosCallback(GLFWwindow* window, double xpos, double ypos) { Vec mousePos = Vec(xpos, ypos).div(gPixelRatio / gWindowRatio).round(); Vec mouseDelta = mousePos.minus(gMousePos); int cursorMode = glfwGetInputMode(gWindow, GLFW_CURSOR); (void) cursorMode; #ifdef ARCH_MAC // Workaround for Mac. We can't use GLFW_CURSOR_DISABLED because it's buggy, so implement it on our own. // This is not an ideal implementation. For example, if the user drags off the screen, the new mouse position will be clamped. if (cursorMode == GLFW_CURSOR_HIDDEN) { // CGSetLocalEventsSuppressionInterval(0.0); glfwSetCursorPos(gWindow, gMousePos.x, gMousePos.y); CGAssociateMouseAndMouseCursorPosition(true); mousePos = gMousePos; } // Because sometimes the cursor turns into an arrow when its position is on the boundary of the window glfwSetCursor(gWindow, NULL); #endif gMousePos = mousePos; event::gContext->handleHover(mousePos, mouseDelta); } void cursorEnterCallback(GLFWwindow* window, int entered) { if (!entered) { event::gContext->handleLeave(); } } void scrollCallback(GLFWwindow *window, double x, double y) { Vec scrollDelta = Vec(x, y); #if ARCH_LIN || ARCH_WIN if (windowIsShiftPressed()) scrollDelta = Vec(y, x); #endif scrollDelta = scrollDelta.mult(50.0); event::gContext->handleScroll(gMousePos, scrollDelta); } void charCallback(GLFWwindow *window, unsigned int codepoint) { event::gContext->handleText(gMousePos, codepoint); } void keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) { event::gContext->handleKey(gMousePos, key, scancode, action, mods); // Keyboard MIDI driver if (!(mods & (GLFW_MOD_SHIFT | GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER))) { if (action == GLFW_PRESS) { keyboard::press(key); } else if (action == GLFW_RELEASE) { keyboard::release(key); } } } void dropCallback(GLFWwindow *window, int count, const char **paths) { std::vector pathsVec; for (int i = 0; i < count; i++) { pathsVec.push_back(paths[i]); } event::gContext->handleDrop(gMousePos, pathsVec); } void errorCallback(int error, const char *description) { WARN("GLFW error %d: %s", error, description); } void renderGui() { int width, height; glfwGetFramebufferSize(gWindow, &width, &height); // Update and render nvgBeginFrame(gVg, width, height, gPixelRatio); nvgReset(gVg); nvgScale(gVg, gPixelRatio, gPixelRatio); event::gContext->rootWidget->draw(gVg); glViewport(0, 0, width, height); glClearColor(0.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); nvgEndFrame(gVg); glfwSwapBuffers(gWindow); } void windowInit() { int err; // Set up GLFW glfwSetErrorCallback(errorCallback); err = glfwInit(); if (err != GLFW_TRUE) { osdialog_message(OSDIALOG_ERROR, OSDIALOG_OK, "Could not initialize GLFW."); exit(1); } #if defined NANOVG_GL2 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); #elif defined NANOVG_GL3 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); #endif glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE); glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_TRUE); lastWindowTitle = ""; gWindow = glfwCreateWindow(640, 480, lastWindowTitle.c_str(), NULL, NULL); if (!gWindow) { osdialog_message(OSDIALOG_ERROR, OSDIALOG_OK, "Cannot open window with OpenGL 2.0 renderer. Does your graphics card support OpenGL 2.0 or greater? If so, make sure you have the latest graphics drivers installed."); exit(1); } glfwMakeContextCurrent(gWindow); glfwSwapInterval(1); glfwSetInputMode(gWindow, GLFW_LOCK_KEY_MODS, 1); glfwSetWindowSizeCallback(gWindow, windowSizeCallback); glfwSetMouseButtonCallback(gWindow, mouseButtonStickyCallback); // Call this ourselves, but on every frame instead of only when the mouse moves // glfwSetCursorPosCallback(gWindow, cursorPosCallback); glfwSetCursorEnterCallback(gWindow, cursorEnterCallback); glfwSetScrollCallback(gWindow, scrollCallback); glfwSetCharCallback(gWindow, charCallback); glfwSetKeyCallback(gWindow, keyCallback); glfwSetDropCallback(gWindow, dropCallback); // Set up GLEW glewExperimental = GL_TRUE; err = glewInit(); if (err != GLEW_OK) { osdialog_message(OSDIALOG_ERROR, OSDIALOG_OK, "Could not initialize GLEW. Does your graphics card support OpenGL 2.0 or greater? If so, make sure you have the latest graphics drivers installed."); exit(1); } // GLEW generates GL error because it calls glGetString(GL_EXTENSIONS), we'll consume it here. glGetError(); glfwSetWindowSizeLimits(gWindow, 640, 480, GLFW_DONT_CARE, GLFW_DONT_CARE); // Set up NanoVG int nvgFlags = NVG_ANTIALIAS; #if defined NANOVG_GL2 gVg = nvgCreateGL2(nvgFlags); #elif defined NANOVG_GL3 gVg = nvgCreateGL3(nvgFlags); #elif defined NANOVG_GLES2 gVg = nvgCreateGLES2(nvgFlags); #endif assert(gVg); #if defined NANOVG_GL2 gFramebufferVg = nvgCreateGL2(nvgFlags); #elif defined NANOVG_GL3 gFramebufferVg = nvgCreateGL3(nvgFlags); #elif defined NANOVG_GLES2 gFramebufferVg = nvgCreateGLES2(nvgFlags); #endif assert(gFramebufferVg); // Set up Blendish gGuiFont = Font::load(asset::global("res/fonts/DejaVuSans.ttf")); bndSetFont(gGuiFont->handle); windowSetTheme(nvgRGB(0x33, 0x33, 0x33), nvgRGB(0xf0, 0xf0, 0xf0)); } void windowDestroy() { gGuiFont.reset(); #if defined NANOVG_GL2 nvgDeleteGL2(gVg); #elif defined NANOVG_GL3 nvgDeleteGL3(gVg); #elif defined NANOVG_GLES2 nvgDeleteGLES2(gVg); #endif #if defined NANOVG_GL2 nvgDeleteGL2(gFramebufferVg); #elif defined NANOVG_GL3 nvgDeleteGL3(gFramebufferVg); #elif defined NANOVG_GLES2 nvgDeleteGLES2(gFramebufferVg); #endif glfwDestroyWindow(gWindow); glfwTerminate(); } void windowRun() { assert(gWindow); gGuiFrame = 0; while(!glfwWindowShouldClose(gWindow)) { double startTime = glfwGetTime(); gGuiFrame++; // Poll events glfwPollEvents(); { double xpos, ypos; glfwGetCursorPos(gWindow, &xpos, &ypos); cursorPosCallback(gWindow, xpos, ypos); } mouseButtonStickyPop(); gamepad::step(); // Set window title std::string windowTitle; windowTitle = gApplicationName; windowTitle += " "; windowTitle += gApplicationVersion; if (!gRackWidget->lastPath.empty()) { windowTitle += " - "; windowTitle += string::filename(gRackWidget->lastPath); } if (windowTitle != lastWindowTitle) { glfwSetWindowTitle(gWindow, windowTitle.c_str()); lastWindowTitle = windowTitle; } // Get desired scaling float pixelRatio; glfwGetWindowContentScale(gWindow, &pixelRatio, NULL); pixelRatio = roundf(pixelRatio); if (pixelRatio != gPixelRatio) { event::gContext->handleZoom(); gPixelRatio = pixelRatio; } // Get framebuffer/window ratio int width, height; glfwGetFramebufferSize(gWindow, &width, &height); int windowWidth, windowHeight; glfwGetWindowSize(gWindow, &windowWidth, &windowHeight); gWindowRatio = (float)width / windowWidth; event::gContext->rootWidget->box.size = Vec(width, height).div(gPixelRatio); // Step scene event::gContext->rootWidget->step(); // Render bool visible = glfwGetWindowAttrib(gWindow, GLFW_VISIBLE) && !glfwGetWindowAttrib(gWindow, GLFW_ICONIFIED); if (visible) { renderGui(); } // Limit framerate manually if vsync isn't working double endTime = glfwGetTime(); double frameTime = endTime - startTime; double minTime = 1.0 / 90.0; if (frameTime < minTime) { std::this_thread::sleep_for(std::chrono::duration(minTime - frameTime)); } endTime = glfwGetTime(); // INFO("%lf fps", 1.0 / (endTime - startTime)); } } void windowClose() { glfwSetWindowShouldClose(gWindow, GLFW_TRUE); } void windowCursorLock() { if (gAllowCursorLock) { #ifdef ARCH_MAC glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); #else glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED); #endif } } void windowCursorUnlock() { if (gAllowCursorLock) { glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL); } } bool windowIsModPressed() { #ifdef ARCH_MAC return glfwGetKey(gWindow, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS || glfwGetKey(gWindow, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS; #else return glfwGetKey(gWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(gWindow, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS; #endif } bool windowIsShiftPressed() { return glfwGetKey(gWindow, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || glfwGetKey(gWindow, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS; } Vec windowGetWindowSize() { int width, height; glfwGetWindowSize(gWindow, &width, &height); return Vec(width, height); } void windowSetWindowSize(Vec size) { int width = size.x; int height = size.y; glfwSetWindowSize(gWindow, width, height); } Vec windowGetWindowPos() { int x, y; glfwGetWindowPos(gWindow, &x, &y); return Vec(x, y); } void windowSetWindowPos(Vec pos) { int x = pos.x; int y = pos.y; glfwSetWindowPos(gWindow, x, y); } bool windowIsMaximized() { return glfwGetWindowAttrib(gWindow, GLFW_MAXIMIZED); } void windowSetTheme(NVGcolor bg, NVGcolor fg) { // Assume dark background and light foreground BNDwidgetTheme w; w.outlineColor = bg; w.itemColor = fg; w.innerColor = bg; w.innerSelectedColor = color::plus(bg, nvgRGB(0x30, 0x30, 0x30)); w.textColor = fg; w.textSelectedColor = fg; w.shadeTop = 0; w.shadeDown = 0; BNDtheme t; t.backgroundColor = color::plus(bg, nvgRGB(0x30, 0x30, 0x30)); t.regularTheme = w; t.toolTheme = w; t.radioTheme = w; t.textFieldTheme = w; t.optionTheme = w; t.choiceTheme = w; t.numberFieldTheme = w; t.sliderTheme = w; t.scrollBarTheme = w; t.tooltipTheme = w; t.menuTheme = w; t.menuItemTheme = w; t.sliderTheme.itemColor = bg; t.sliderTheme.innerColor = color::plus(bg, nvgRGB(0x50, 0x50, 0x50)); t.sliderTheme.innerSelectedColor = color::plus(bg, nvgRGB(0x60, 0x60, 0x60)); t.textFieldTheme = t.sliderTheme; t.textFieldTheme.textColor = color::minus(bg, nvgRGB(0x20, 0x20, 0x20)); t.textFieldTheme.textSelectedColor = t.textFieldTheme.textColor; t.scrollBarTheme.itemColor = color::plus(bg, nvgRGB(0x50, 0x50, 0x50)); t.scrollBarTheme.innerColor = bg; t.menuTheme.innerColor = color::minus(bg, nvgRGB(0x10, 0x10, 0x10)); t.menuTheme.textColor = color::minus(fg, nvgRGB(0x50, 0x50, 0x50)); t.menuTheme.textSelectedColor = t.menuTheme.textColor; bndSetTheme(t); } static int windowX = 0; static int windowY = 0; static int windowWidth = 0; static int windowHeight = 0; void windowSetFullScreen(bool fullScreen) { if (windowGetFullScreen()) { glfwSetWindowMonitor(gWindow, NULL, windowX, windowY, windowWidth, windowHeight, GLFW_DONT_CARE); } else { glfwGetWindowPos(gWindow, &windowX, &windowY); glfwGetWindowSize(gWindow, &windowWidth, &windowHeight); GLFWmonitor *monitor = glfwGetPrimaryMonitor(); const GLFWvidmode* mode = glfwGetVideoMode(monitor); glfwSetWindowMonitor(gWindow, monitor, 0, 0, mode->width, mode->height, mode->refreshRate); } } bool windowGetFullScreen() { GLFWmonitor *monitor = glfwGetWindowMonitor(gWindow); return monitor != NULL; } //////////////////// // resources //////////////////// Font::Font(const std::string &filename) { handle = nvgCreateFont(gVg, filename.c_str(), filename.c_str()); if (handle >= 0) { INFO("Loaded font %s", filename.c_str()); } else { WARN("Failed to load font %s", filename.c_str()); } } Font::~Font() { // There is no NanoVG deleteFont() function yet, so do nothing } std::shared_ptr Font::load(const std::string &filename) { static std::map> cache; auto sp = cache[filename].lock(); if (!sp) cache[filename] = sp = std::make_shared(filename); return sp; } //////////////////// // Image //////////////////// Image::Image(const std::string &filename) { handle = nvgCreateImage(gVg, filename.c_str(), NVG_IMAGE_REPEATX | NVG_IMAGE_REPEATY); if (handle > 0) { INFO("Loaded image %s", filename.c_str()); } else { WARN("Failed to load image %s", filename.c_str()); } } Image::~Image() { // TODO What if handle is invalid? nvgDeleteImage(gVg, handle); } std::shared_ptr Image::load(const std::string &filename) { static std::map> cache; auto sp = cache[filename].lock(); if (!sp) cache[filename] = sp = std::make_shared(filename); return sp; } //////////////////// // SVG //////////////////// SVG::SVG(const std::string &filename) { handle = nsvgParseFromFile(filename.c_str(), "px", SVG_DPI); if (handle) { INFO("Loaded SVG %s", filename.c_str()); } else { WARN("Failed to load SVG %s", filename.c_str()); } } SVG::~SVG() { nsvgDelete(handle); } std::shared_ptr SVG::load(const std::string &filename) { static std::map> cache; auto sp = cache[filename].lock(); if (!sp) cache[filename] = sp = std::make_shared(filename); return sp; } } // namespace rack