diff --git a/src/CardinalCommon.hpp b/src/CardinalCommon.hpp index 6824c30..fe25487 100644 --- a/src/CardinalCommon.hpp +++ b/src/CardinalCommon.hpp @@ -37,6 +37,10 @@ namespace ui { struct Menu; } +namespace window { +void generateScreenshot(); +} + } // namespace rack namespace patchUtils { diff --git a/src/CardinalPlugin.cpp b/src/CardinalPlugin.cpp index 43b9f65..8ca8ee7 100644 --- a/src/CardinalPlugin.cpp +++ b/src/CardinalPlugin.cpp @@ -49,10 +49,10 @@ #ifndef HEADLESS # include "WindowParameters.hpp" -static const constexpr uint kCardinalStateCount = 3; // patch, text, windowSize +static const constexpr uint kCardinalStateCount = 4; // patch, screenshot, comment, windowSize #else # define kWindowParameterCount 0 -static const constexpr uint kCardinalStateCount = 2; // patch, text +static const constexpr uint kCardinalStateCount = 3; // patch, screenshot, comment #endif namespace rack { @@ -698,10 +698,13 @@ protected: stateKey = "patch"; break; case 1: - stateKey = "text"; + stateKey = "screenshot"; break; - #ifndef HEADLESS case 2: + stateKey = "comment"; + break; + #ifndef HEADLESS + case 3: stateKey = "windowSize"; break; #endif @@ -712,7 +715,8 @@ protected: { switch (index) { - case 1: + case 1: // screenshot + case 2: // comment return kStateIsHostVisible; default: return 0x0; diff --git a/src/override/MenuBar.cpp b/src/override/MenuBar.cpp index 23647cc..14a4901 100644 --- a/src/override/MenuBar.cpp +++ b/src/override/MenuBar.cpp @@ -479,6 +479,8 @@ struct ViewButton : MenuButton { menu->addChild(createBoolPtrMenuItem("Lock module positions", "", &settings::lockModules)); + menu->addChild(new ui::MenuSeparator); + static const std::vector rateLimitLabels = { "None", "2x", diff --git a/src/override/Scene.cpp b/src/override/Scene.cpp index c6d77d8..ad02f31 100644 --- a/src/override/Scene.cpp +++ b/src/override/Scene.cpp @@ -185,11 +185,20 @@ math::Vec Scene::getMousePos() { void Scene::step() { + if (APP->window->isFullScreen()) { + // Expand RackScrollWidget to cover entire screen if fullscreen + rackScroll->box.pos.y = 0; + } + else { + // Always show MenuBar if not fullscreen + menuBar->show(); + rackScroll->box.pos.y = menuBar->box.size.y; + } + internal->resizeHandle->box.pos = box.size.minus(internal->resizeHandle->box.size); // Resize owned descendants menuBar->box.size.x = box.size.x; - rackScroll->box.pos.y = menuBar->box.size.y; rackScroll->box.size = box.size.minus(rackScroll->box.pos); // Scroll RackScrollWidget with arrow keys @@ -334,6 +343,10 @@ void Scene::onHoverKey(const HoverKeyEvent& e) { patchUtils::deployToRemote(); e.consume(this); } + if (e.key == GLFW_KEY_F9 && (e.mods & RACK_MOD_MASK) == 0) { + window::generateScreenshot(); + e.consume(this); + } // Module selections if (e.keyName == "a" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { diff --git a/src/override/Window.cpp b/src/override/Window.cpp index 87c0502..a9b0bd1 100644 --- a/src/override/Window.cpp +++ b/src/override/Window.cpp @@ -36,15 +36,25 @@ #include #include #include -#include // used in Window::screenshot -#include // used in Window::screenshot +#include #ifdef NDEBUG # undef DEBUG #endif +// comment out if wanting to generate a local screenshot.png +#define STBI_WRITE_NO_STDIO + +// uncomment to generate screenshots without the rack rail background (ie, transparent) +// #define CARDINAL_TRANSPARENT_SCREENSHOTS + +// used in Window::screenshot +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "../src/Rack/dep/glfw/deps/stb_image_write.h" + #include "DistrhoUI.hpp" #include "Application.hpp" +#include "extra/String.hpp" #include "../CardinalCommon.hpp" #include "../WindowParameters.hpp" @@ -119,6 +129,15 @@ std::shared_ptr Image::load(const std::string& filename) { } +enum ScreenshotStep { + kScreenshotStepNone, + kScreenshotStepStarted, + kScreenshotStepFirstPass, + kScreenshotStepSecondPass, + kScreenshotStepSaving +}; + + struct Window::Internal { std::string lastWindowTitle; @@ -139,6 +158,7 @@ struct Window::Internal { int frame = 0; int frameSwapInterval = 1; + int generateScreenshotStep = kScreenshotStepNone; double monitorRefreshRate = 60.0; double frameTime = 0.0; double lastFrameDuration = 0.0; @@ -342,6 +362,26 @@ void Window::run() { } +static void Window__flipBitmap(uint8_t* pixels, int width, int height, int depth) { + for (int y = 0; y < height / 2; y++) { + const int flipY = height - y - 1; + uint8_t tmp[width * depth]; + std::memcpy(tmp, &pixels[y * width * depth], width * depth); + std::memcpy(&pixels[y * width * depth], &pixels[flipY * width * depth], width * depth); + std::memcpy(&pixels[flipY * width * depth], tmp, width * depth); + } +} + + +static void Window__writeImagePNG(void* context, void* data, int size) { + USE_NAMESPACE_DISTRHO + UI* const ui = static_cast(context); + String encodedPNG("data:image/png;base64,"); + encodedPNG += String::asBase64(data, size); + ui->setState("screenshot", encodedPNG.buffer()); +} + + void Window::step() { DISTRHO_SAFE_ASSERT_RETURN(internal->ui != nullptr,); @@ -378,6 +418,16 @@ void Window::step() { APP->event->handleDirty(); } + // Hide menu and background if generating screenshot + if (internal->generateScreenshotStep == kScreenshotStepStarted) { +#ifdef CARDINAL_TRANSPARENT_SCREENSHOTS + APP->scene->menuBar->hide(); + APP->scene->rack->children.front()->hide(); +#else + internal->generateScreenshotStep = kScreenshotStepSecondPass; +#endif + } + // Get framebuffer/window ratio int winWidth = internal->ui->getWidth(); int winHeight = internal->ui->getHeight(); @@ -388,7 +438,7 @@ void Window::step() { if (APP->scene) { // DEBUG("%f %f %d %d", pixelRatio, windowRatio, fbWidth, winWidth); // Resize scene - APP->scene->box.size = math::Vec(fbWidth, fbHeight).div(pixelRatio); + APP->scene->box.size = math::Vec(fbWidth, fbHeight).div(newPixelRatio); // Step scene APP->scene->step(); @@ -396,7 +446,7 @@ void Window::step() { // Render scene { // Update and render - nvgScale(vg, pixelRatio, pixelRatio); + nvgScale(vg, newPixelRatio, newPixelRatio); // Draw scene widget::Widget::DrawArgs args; @@ -405,12 +455,52 @@ void Window::step() { APP->scene->draw(args); glViewport(0, 0, fbWidth, fbHeight); +#ifdef CARDINAL_TRANSPARENT_SCREENSHOTS + glClearColor(0.0, 0.0, 0.0, 0.0); +#else glClearColor(0.0, 0.0, 0.0, 1.0); +#endif glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); } } ++internal->frame; + + if (internal->generateScreenshotStep != kScreenshotStepNone) { + ++internal->generateScreenshotStep; + + int y = 0; +#ifndef CARDINAL_TRANSPARENT_SCREENSHOTS + y = APP->scene->menuBar->box.size.y * newPixelRatio; +#endif + + // Allocate pixel color buffer + uint8_t* const pixels = new uint8_t[winHeight * winWidth * 4]; + + // glReadPixels defaults to GL_BACK, but the back-buffer is unstable, so use the front buffer (what the user sees) + glReadBuffer(GL_FRONT); + glReadPixels(0, 0, winWidth, winHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + + if (internal->generateScreenshotStep == kScreenshotStepSaving) + { + // Write pixels to PNG + const int stride = winWidth * 4; + const uint8_t* const pixelsWithOffset = pixels + (stride * y); + Window__flipBitmap(pixels, winWidth, winHeight, 4); +#ifdef STBI_WRITE_NO_STDIO + stbi_write_png_to_func(Window__writeImagePNG, internal->ui, + winWidth, winHeight - y, 4, pixelsWithOffset, stride); +#else + stbi_write_png("screenshot.png", winWidth, winHeight - y, 4, pixelsWithOffset, stride); +#endif + + internal->generateScreenshotStep = kScreenshotStepNone; + APP->scene->menuBar->show(); + APP->scene->rack->children.front()->show(); + } + + delete[] pixels; + } } @@ -456,7 +546,11 @@ void Window::setFullScreen(bool) { bool Window::isFullScreen() { +#ifdef CARDINAL_TRANSPARENT_SCREENSHOTS + return internal->generateScreenshotStep != kScreenshotStepNone; +#else return false; +#endif } @@ -533,6 +627,11 @@ int& Window::fbCount() { } +void generateScreenshot() { + APP->window->internal->generateScreenshotStep = kScreenshotStepStarted; +} + + void init() { }