diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index d8fa1cfed6..b3daa5dd7c 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -308,7 +308,7 @@ jobs: - name: Configure working-directory: ${{github.workspace}} - run: CXX=g++ cmake -Bbuild -GNinja -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DENABLE_WAYLAND=1 + run: CXX=g++ cmake -Bbuild -GNinja -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build working-directory: ${{github.workspace}} @@ -406,7 +406,7 @@ jobs: - name: Configure working-directory: ${{github.workspace}} - run: CXX=g++ cmake -Bbuild -GNinja -DNANOVG_GLES_IMPLEMENTATION=1 -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DENABLE_WAYLAND=1 + run: CXX=g++ cmake -Bbuild -GNinja -DNANOVG_GLES_IMPLEMENTATION=1 -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build working-directory: ${{github.workspace}} run: cmake --build build --config Release diff --git a/CMakeLists.txt b/CMakeLists.txt index 98565a6124..f1f0b5ae58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,11 @@ option(ENABLE_WAYLAND "Enable native wayland support (experimental)" OFF) option(MACOS_LEGACY "Enable support for old macOS versions (lowers OSX_DEPLOYMENT TARGET and disables Metal" OFF) option(VERBOSE "" OFF) +if(ENABLE_ASAN) + add_compile_options(-fsanitize=address) + add_link_options(-fsanitize=address) +endif() + if(CMAKE_GENERATOR STREQUAL "Ninja" AND MSVC) target_compile_options(pd PRIVATE /EHsc) endif() @@ -187,11 +192,6 @@ elseif (MSVC) add_link_options(/OPT:REF /OPT:ICF /INCREMENTAL:NO) endif() -if(ENABLE_ASAN) - add_compile_options(-fsanitize=address) - add_link_options(-fsanitize=address) -endif() - if(MSVC) add_compile_options(/MP /wd4244 /wd4311 /wd4003 /wd4047 /wd4477 /wd4068 /wd4133 /wd4311) add_compile_options("$<$,$>:/bigobj>") @@ -831,13 +831,20 @@ elseif(APPLE) set(MACOS_COMPAT_LINKER_FLAGS "-Wl,-ld_classic") endif() + # LuaJIT ships its own DWARF unwind personality routine (_lj_err_unwind_dwarf). + # Together with the gcc/g++/objc personalities this exceeds what the modern + # linker's compact-unwind format can encode. Release builds dodge this via + # -ld_classic above, but that flag is gated to non-Debug configs, so Debug and + # sanitizer builds must fall back to DWARF unwind info instead of compact unwind. + set(MACOS_LUAJIT_UNWIND_FLAGS "$<$:-Wl,-no_compact_unwind>") + if(NOT "${CMAKE_SYSTEM_NAME}" MATCHES "iOS") set(LINK_CARBON "-framework Carbon") endif() - target_link_libraries(plugdata_standalone PRIVATE plugdata_core pd-src externals ${LINK_CARBON} $<$>:${MACOS_COMPAT_LINKER_FLAGS}>) - target_link_libraries(plugdata_midi PRIVATE plugdata_core pd-src-multi externals-multi ${LINK_CARBON} $<$>:${MACOS_COMPAT_LINKER_FLAGS}>) - target_link_libraries(plugdata PRIVATE plugdata_core pd-src-multi externals-multi ${LINK_CARBON} $<$>:${MACOS_COMPAT_LINKER_FLAGS}>) - target_link_libraries(plugdata_fx PRIVATE plugdata_core pd-src-multi externals-multi ${LINK_CARBON} $<$>:${MACOS_COMPAT_LINKER_FLAGS}>) + target_link_libraries(plugdata_standalone PRIVATE plugdata_core pd-src externals ${LINK_CARBON} $<$>:${MACOS_COMPAT_LINKER_FLAGS}> ${MACOS_LUAJIT_UNWIND_FLAGS}) + target_link_libraries(plugdata_midi PRIVATE plugdata_core pd-src-multi externals-multi ${LINK_CARBON} $<$>:${MACOS_COMPAT_LINKER_FLAGS}> ${MACOS_LUAJIT_UNWIND_FLAGS}) + target_link_libraries(plugdata PRIVATE plugdata_core pd-src-multi externals-multi ${LINK_CARBON} $<$>:${MACOS_COMPAT_LINKER_FLAGS}> ${MACOS_LUAJIT_UNWIND_FLAGS}) + target_link_libraries(plugdata_fx PRIVATE plugdata_core pd-src-multi externals-multi ${LINK_CARBON} $<$>:${MACOS_COMPAT_LINKER_FLAGS}> ${MACOS_LUAJIT_UNWIND_FLAGS}) else() target_link_libraries(plugdata PRIVATE plugdata_core pd-multi) target_link_libraries(plugdata_fx PRIVATE plugdata_core pd-multi) diff --git a/Libraries/JUCE b/Libraries/JUCE index fb35862589..5320090a15 160000 --- a/Libraries/JUCE +++ b/Libraries/JUCE @@ -1 +1 @@ -Subproject commit fb35862589d4d0a1dc8b58252d67e0d33df6e6ed +Subproject commit 5320090a158189eedeb63e9d60d5e845da956509 diff --git a/Libraries/clap-juce-extensions b/Libraries/clap-juce-extensions index 02f91b7988..c1a5ad025f 160000 --- a/Libraries/clap-juce-extensions +++ b/Libraries/clap-juce-extensions @@ -1 +1 @@ -Subproject commit 02f91b7988298f7f1f05c706da16e1d9da852a87 +Subproject commit c1a5ad025f95d01e03267857fa8276ebeed16500 diff --git a/Libraries/nanovg b/Libraries/nanovg index 8249b37cdb..933df5df88 160000 --- a/Libraries/nanovg +++ b/Libraries/nanovg @@ -1 +1 @@ -Subproject commit 8249b37cdb37255f8f9e63c1328d93ce2836ce75 +Subproject commit 933df5df8834b6b5deca192fe5274360d44fae00 diff --git a/Libraries/pd-else b/Libraries/pd-else index 76cfe03a9c..7adfbe7b95 160000 --- a/Libraries/pd-else +++ b/Libraries/pd-else @@ -1 +1 @@ -Subproject commit 76cfe03a9c0b202d1317674029ceec85c3b1b4a2 +Subproject commit 7adfbe7b95338b90114b5334713d33af5e7d0ed8 diff --git a/Resources/Fonts/InterBold.ttf b/Resources/Fonts/InterBold.ttf deleted file mode 100644 index 15e908f236..0000000000 Binary files a/Resources/Fonts/InterBold.ttf and /dev/null differ diff --git a/Resources/Fonts/InterRegular.ttf b/Resources/Fonts/InterRegular.ttf deleted file mode 100644 index c544be4784..0000000000 Binary files a/Resources/Fonts/InterRegular.ttf and /dev/null differ diff --git a/Resources/Fonts/InterSemiBold.ttf b/Resources/Fonts/InterSemiBold.ttf deleted file mode 100644 index a32e2b8d21..0000000000 Binary files a/Resources/Fonts/InterSemiBold.ttf and /dev/null differ diff --git a/Resources/Fonts/InterTabular.ttf b/Resources/Fonts/InterTabular.ttf deleted file mode 100644 index 0d1d35b501..0000000000 Binary files a/Resources/Fonts/InterTabular.ttf and /dev/null differ diff --git a/Resources/Fonts/InterVariable.ttf b/Resources/Fonts/InterVariable.ttf index ec3164efa8..4ab79e0102 100644 Binary files a/Resources/Fonts/InterVariable.ttf and b/Resources/Fonts/InterVariable.ttf differ diff --git a/Resources/Fonts/RobotoMono-Bold.ttf b/Resources/Fonts/RobotoMono-Bold.ttf deleted file mode 100644 index bef439f3b9..0000000000 Binary files a/Resources/Fonts/RobotoMono-Bold.ttf and /dev/null differ diff --git a/Resources/Fonts/RobotoMono-Regular.ttf b/Resources/Fonts/RobotoMono-Regular.ttf deleted file mode 100644 index f376abb7a0..0000000000 Binary files a/Resources/Fonts/RobotoMono-Regular.ttf and /dev/null differ diff --git a/Resources/Fonts/RobotoMonoVariable.ttf b/Resources/Fonts/RobotoMonoVariable.ttf new file mode 100644 index 0000000000..3a2d704bbb Binary files /dev/null and b/Resources/Fonts/RobotoMonoVariable.ttf differ diff --git a/Resources/Scripts/package_resources.py b/Resources/Scripts/package_resources.py index 29c2d3db47..9430342230 100644 --- a/Resources/Scripts/package_resources.py +++ b/Resources/Scripts/package_resources.py @@ -347,13 +347,8 @@ def generateBinaryDataArchive(output_dir, file_list): resources = [ project_root + "/Resources/Fonts/IconFont.ttf", - project_root + "/Resources/Fonts/InterTabular.ttf", - project_root + "/Resources/Fonts/InterBold.ttf", - project_root + "/Resources/Fonts/InterSemiBold.ttf", project_root + "/Resources/Fonts/InterVariable.ttf", - project_root + "/Resources/Fonts/InterRegular.ttf", - project_root + "/Resources/Fonts/RobotoMono-Regular.ttf", - project_root + "/Resources/Fonts/RobotoMono-Bold.ttf", + project_root + "/Resources/Fonts/RobotoMonoVariable.ttf", project_root + "/Resources/Icons/plugdata_large_logo.png", project_root + "/Resources/Icons/plugdata_logo.png", "Documentation.bin", diff --git a/Source/Canvas.cpp b/Source/Canvas.cpp index 6e8bf531b4..847bfe9073 100644 --- a/Source/Canvas.cpp +++ b/Source/Canvas.cpp @@ -171,7 +171,7 @@ class ObjectsResizer final : public Component auto iCol = nvgColour(PlugDataColours::objectSelectedOutlineColour); iCol.a = 5; // Make the inner colour semi-transparent - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), iCol, nvgColour(PlugDataColours::objectSelectedOutlineColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), iCol, nvgColour(PlugDataColours::objectSelectedOutlineColour), Corners::objectCornerRadius); // Draw handles at edge auto getCorners = [this] { @@ -184,30 +184,17 @@ class ObjectsResizer final : public Component return corners; }; - auto& resizeHandleImage = cnv->resizeHandleImage; int angle = 360; for (auto& corner : getCorners()) { NVGScopedState scopedState(nvg); // Rotate around centre - nvgTranslate(nvg, corner.getCentreX(), corner.getCentreY()); - nvgRotate(nvg, degreesToRadians(angle)); - nvgTranslate(nvg, -4.5f, -4.5f); - - nvgBeginPath(nvg); - nvgRect(nvg, 0, 0, 9, 9); - nvgFillPaint(nvg, nvgImageAlphaPattern(nvg, 0, 0, 9, 9, 0, resizeHandleImage.getImageId(), nvgColour(PlugDataColours::objectSelectedOutlineColour))); - nvgFill(nvg); + nanovg::nvgTranslate(nvg, corner.getCentreX(), corner.getCentreY()); + nanovg::nvgRotate(nvg, degreesToRadians(angle)); + nanovg::nvgTranslate(nvg, -4.5f, -4.5f); + + cnv->renderResizeHandle(nvg, nvgColour(PlugDataColours::objectSelectedOutlineColour)); angle -= 90; } -// #define SPACER_TEXT -#ifdef SPACER_TEXT - nvgBeginPath(nvg); - auto textPos = getPosition().translated(0, -25); - nvgFontSize(nvg, 20.0f); - nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); - nvgFillColor(nvg, nvgRGBA(240, 240, 240, 255)); - nvgText(nvg, textPos.x, textPos.y, String("Spacer size: " + String(spacer + 1.0f)).toRawUTF8(), nullptr); -#endif } private: @@ -442,38 +429,20 @@ void Canvas::updateFramebuffers(NVGcontext* nvg) auto const pixelScale = editor->getRenderScale(); auto zoom = getValue(zoomScale); - constexpr int resizerLogicalSize = 9; - float const viewScale = pixelScale * zoom; - int const resizerBufferSize = resizerLogicalSize * viewScale; - - if (resizeHandleImage.needsUpdate(resizerBufferSize, resizerBufferSize)) { - resizeHandleImage = NVGImage(nvg, resizerBufferSize, resizerBufferSize, [viewScale](Graphics& g) { - g.addTransform(AffineTransform::scale(viewScale, viewScale)); - auto const b = Rectangle(0, 0, 9, 9); - // use the path with a hole in it to exclude the inner rounded rect from painting - Path outerArea; - outerArea.addRectangle(b); - outerArea.setUsingNonZeroWinding(false); - - Path innerArea; - auto const innerRect = b.translated(Object::margin / 2, Object::margin / 2); - innerArea.addRoundedRectangle(innerRect, Corners::objectCornerRadius); - outerArea.addPath(innerArea); - g.reduceClipRegion(outerArea); - - g.setColour(Colours::white); // For alpha image colour isn't important - g.fillRoundedRectangle(0.0f, 0.0f, 9.0f, 9.0f, Corners::resizeHanleCornerRadius); }, NVGImage::AlphaImage); - } - auto gridLogicalSize = objectGrid.gridSize ? objectGrid.gridSize : 25; - auto gridSizeCommon = 300; + auto constexpr gridSizeCommon = 300; auto const gridBufferSize = gridSizeCommon * pixelScale * zoom; if (dotsLargeImage.needsUpdate(gridBufferSize, gridBufferSize) || lastObjectGridSize != gridLogicalSize) { lastObjectGridSize = gridLogicalSize; - dotsLargeImage = NVGImage(nvg, gridBufferSize, gridBufferSize, [zoom, viewScale, gridLogicalSize, gridSizeCommon](Graphics& g) { - g.addTransform(AffineTransform::scale(viewScale, viewScale)); + dotsLargeImage.renderToFramebuffer(nvg, gridBufferSize, gridBufferSize, [gridBufferSize, gridLogicalSize, zoom, pixelScale](NVGcontext* nvg){ + + nanovg::viewport(nvg, 0, 0, gridBufferSize, gridBufferSize); + nanovg::clear(nvg); + nanovg::nvgBeginFrame(nvg, gridSizeCommon * zoom, gridSizeCommon * zoom, pixelScale); + nanovg::nvgScale(nvg, zoom, zoom); + float const ellipseRadius = zoom < 1.0f ? jmap(zoom, 0.25f, 1.0f, 3.0f, 1.0f) : 1.0f; int decim = 0; @@ -510,26 +479,72 @@ void Canvas::updateFramebuffers(NVGcontext* nvg) auto markingColour = PlugDataColours::canvasDotsColour.interpolatedWith(PlugDataColours::canvasBackgroundColour, 0.2f); auto const majorDotColour = markingColour.withAlpha(std::min(zoom * 0.8f, 1.0f)); - g.setColour(majorDotColour); + nanovg::nvgFillColor(nvg, nvgColour(majorDotColour)); + // Draw ellipses on the grid for (int x = 0; x <= gridSizeCommon; x += gridLogicalSize) { for (int y = 0; y <= gridSizeCommon; y += gridLogicalSize) { + NVGcolor c = nvgColour(majorDotColour); if (decim != 0) { if (x % decim && y % decim) continue; - g.setColour(majorDotColour); if (x % decim == 0 && y % decim == 0) - g.setColour(markingColour); + c = nvgColour(markingColour); } // Add half smallest dot offset so the dot isn't at the edge of the texture // We remove this when we position the texture on the canvas float const centerX = static_cast(x) + 2.5f; float const centerY = static_cast(y) + 2.5f; - g.fillEllipse(centerX - ellipseRadius, centerY - ellipseRadius, ellipseRadius * 2.0f, ellipseRadius * 2.0f); + float const ellipseSize = ellipseRadius * 2.0f; + nanovg::nvgDrawRoundedRect(nvg, centerX - ellipseRadius, centerY - ellipseRadius, ellipseSize, ellipseSize, c, c, ellipseRadius); } - } }, NVGImage::RepeatImage, PlugDataColours::canvasBackgroundColour); + } + + nanovg::nvgGlobalScissor(nvg, 0, 0, gridBufferSize, gridBufferSize); + nanovg::nvgEndFrameWithoutPublishing(nvg); + }, NVG_IMAGE_REPEATX | NVG_IMAGE_REPEATY); + } +} + +void Canvas::render(NVGcontext* nvg) +{ + performRender(nvg, getLocalBounds()); +} + +void Canvas::renderResizeHandle(NVGcontext* nvg, NVGcolor const colour) +{ + nanovg::nvgFillColor(nvg, colour); + + if (!resizeHandlePath.fill()) { + constexpr float resizerLogicalSize = 9.0f; + constexpr float kappa = 0.5522847498307936f; + auto const innerOffset = static_cast(Object::margin) / 2.0f; + auto const outerRadius = jlimit(0.0f, resizerLogicalSize * 0.5f, Corners::resizeHanleCornerRadius); + auto const innerRadius = jlimit(0.0f, resizerLogicalSize - innerOffset, Corners::objectCornerRadius); + auto const outerControl = outerRadius * (1.0f - kappa); + auto const innerControl = innerRadius * (1.0f - kappa); + + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, outerRadius, 0.0f); + nanovg::nvgLineTo(nvg, resizerLogicalSize - outerRadius, 0.0f); + nanovg::nvgBezierTo(nvg, resizerLogicalSize - outerControl, 0.0f, resizerLogicalSize, outerControl, resizerLogicalSize, outerRadius); + nanovg::nvgLineTo(nvg, resizerLogicalSize, innerOffset); + nanovg::nvgLineTo(nvg, innerOffset + innerRadius, innerOffset); + if (innerRadius > 0.0f) { + nanovg::nvgBezierTo(nvg, innerOffset + innerControl, innerOffset, innerOffset, innerOffset + innerControl, innerOffset, innerOffset + innerRadius); + } else { + nanovg::nvgLineTo(nvg, innerOffset, innerOffset); + } + nanovg::nvgLineTo(nvg, innerOffset, resizerLogicalSize); + nanovg::nvgLineTo(nvg, outerRadius, resizerLogicalSize); + nanovg::nvgBezierTo(nvg, outerControl, resizerLogicalSize, 0.0f, resizerLogicalSize - outerControl, 0.0f, resizerLogicalSize - outerRadius); + nanovg::nvgLineTo(nvg, 0.0f, outerRadius); + nanovg::nvgBezierTo(nvg, 0.0f, outerControl, outerControl, 0.0f, outerRadius, 0.0f); + nanovg::nvgClosePath(nvg); + nanovg::nvgFill(nvg); + resizeHandlePath.save(nvg); } } @@ -539,20 +554,20 @@ void Canvas::performRender(NVGcontext* nvg, Rectangle invalidRegion) constexpr auto halfSize = infiniteCanvasSize / 2; auto const zoom = getValue(zoomScale); bool const isLocked = getValue(locked); - nvgSave(nvg); + nanovg::nvgSave(nvg); if (viewport) { - nvgTranslate(nvg, -viewport->getViewPositionX(), -viewport->getViewPositionY()); - nvgScale(nvg, zoom, zoom); + nanovg::nvgTranslate(nvg, -viewport->getViewPositionX(), -viewport->getViewPositionY()); + nanovg::nvgScale(nvg, zoom, zoom); invalidRegion = invalidRegion.translated(viewport->getViewPositionX(), viewport->getViewPositionY()); invalidRegion /= zoom; if (isLocked) { - nvgFillColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour)); - nvgFillRect(nvg, invalidRegion.getX(), invalidRegion.getY(), invalidRegion.getWidth(), invalidRegion.getHeight()); + nanovg::nvgFillColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour)); + nanovg::nvgFillRect(nvg, invalidRegion.getX(), invalidRegion.getY(), invalidRegion.getWidth(), invalidRegion.getHeight()); } else { - nvgBeginPath(nvg); - nvgRect(nvg, 0, 0, infiniteCanvasSize, infiniteCanvasSize); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRect(nvg, 0, 0, infiniteCanvasSize, infiniteCanvasSize); // Use least common multiple of grid sizes: 5,10,15,20,25,30 for texture size for now // We repeat the texture on GPU, this is so the texture does not become too small for GPU processing @@ -562,13 +577,13 @@ void Canvas::performRender(NVGcontext* nvg, Rectangle invalidRegion) constexpr auto gridSizeCommon = 300; NVGScopedState scopedState(nvg); // offset image texture by 2.5f so no dots are on the edge of the texture - nvgTranslate(nvg, canvasOrigin.x - 2.5f, canvasOrigin.x - 2.5f); + nanovg::nvgTranslate(nvg, canvasOrigin.x - 2.5f, canvasOrigin.x - 2.5f); - nvgFillColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour)); // This fixes some glitches but I'm not sure why - nvgFill(nvg); + nanovg::nvgFillColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour)); // This fixes some glitches but I'm not sure why + nanovg::nvgFill(nvg); - nvgFillPaint(nvg, nvgImagePattern(nvg, 0, 0, gridSizeCommon, gridSizeCommon, 0, dotsLargeImage.getImageId(), 1)); - nvgFill(nvg); + nanovg::nvgFillPaint(nvg, nanovg::nvgImagePattern(nvg, 0, 0, gridSizeCommon, gridSizeCommon, 0, dotsLargeImage.getImage(), 1)); + nanovg::nvgFill(nvg); } } } @@ -578,59 +593,59 @@ void Canvas::performRender(NVGcontext* nvg, Rectangle invalidRegion) auto drawBorder = [this, nvg](bool const bg, bool const fg) { if (viewport && (showOrigin || showBorder) && !::getValue(presentationMode)) { NVGScopedState scopedState(nvg); - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); auto const borderWidth = getValue(patchWidth); auto const borderHeight = getValue(patchHeight); constexpr auto pos = Point(halfSize, halfSize); if (bg) { - nvgBeginPath(nvg); - nvgMoveTo(nvg, pos.x, pos.y); - nvgLineTo(nvg, pos.x, pos.y + (showOrigin ? halfSize : borderHeight)); - nvgMoveTo(nvg, pos.x, pos.y); - nvgLineTo(nvg, pos.x + (showOrigin ? halfSize : borderWidth), pos.y); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, pos.x, pos.y); + nanovg::nvgLineTo(nvg, pos.x, pos.y + (showOrigin ? halfSize : borderHeight)); + nanovg::nvgMoveTo(nvg, pos.x, pos.y); + nanovg::nvgLineTo(nvg, pos.x + (showOrigin ? halfSize : borderWidth), pos.y); if (showBorder) { - nvgMoveTo(nvg, pos.x + borderWidth, pos.y); - nvgLineTo(nvg, pos.x + borderWidth, pos.y + borderHeight); - nvgLineTo(nvg, pos.x, pos.y + borderHeight); + nanovg::nvgMoveTo(nvg, pos.x + borderWidth, pos.y); + nanovg::nvgLineTo(nvg, pos.x + borderWidth, pos.y + borderHeight); + nanovg::nvgLineTo(nvg, pos.x, pos.y + borderHeight); } - nvgLineStyle(nvg, NVG_LINE_SOLID); - nvgStrokeColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour)); - nvgStrokeWidth(nvg, 8.0f); - nvgStroke(nvg); + nanovg::nvgLineStyle(nvg, NVG_LINE_SOLID); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour)); + nanovg::nvgStrokeWidth(nvg, 8.0f); + nanovg::nvgStroke(nvg); - nvgFillColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour)); - nvgFillRect(nvg, pos.x - 1.0f, pos.y - 1.0f, 2, 2); + nanovg::nvgFillColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour)); + nanovg::nvgFillRect(nvg, pos.x - 1.0f, pos.y - 1.0f, 2, 2); } - nvgStrokeColor(nvg, nvgColour(PlugDataColours::canvasDotsColour.interpolatedWith(PlugDataColours::canvasBackgroundColour, 0.2f))); - nvgStrokeWidth(nvg, 1.5f); - nvgDashLength(nvg, 8.0f); - nvgLineStyle(nvg, NVG_LINE_DASHED); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::canvasDotsColour.interpolatedWith(PlugDataColours::canvasBackgroundColour, 0.2f))); + nanovg::nvgStrokeWidth(nvg, 1.5f); + nanovg::nvgDashLength(nvg, 8.0f); + nanovg::nvgLineStyle(nvg, NVG_LINE_DASHED); if (fg) { - nvgBeginPath(nvg); - nvgMoveTo(nvg, pos.x, pos.y); - nvgLineTo(nvg, pos.x, pos.y + (showOrigin ? halfSize : borderHeight)); - nvgStroke(nvg); - nvgBeginPath(nvg); - nvgMoveTo(nvg, pos.x, pos.y); - nvgLineTo(nvg, pos.x + (showOrigin ? halfSize : borderWidth), pos.y); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, pos.x, pos.y); + nanovg::nvgLineTo(nvg, pos.x, pos.y + (showOrigin ? halfSize : borderHeight)); + nanovg::nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, pos.x, pos.y); + nanovg::nvgLineTo(nvg, pos.x + (showOrigin ? halfSize : borderWidth), pos.y); + nanovg::nvgStroke(nvg); } if (showBorder && fg) { - nvgStrokeWidth(nvg, 1.5f); - nvgLineStyle(nvg, NVG_LINE_DASHED); - nvgBeginPath(nvg); - nvgMoveTo(nvg, pos.x + borderWidth, pos.y + borderHeight); - nvgLineTo(nvg, pos.x + borderWidth, pos.y); - nvgStroke(nvg); - nvgBeginPath(nvg); - nvgMoveTo(nvg, pos.x + borderWidth, pos.y + borderHeight); - nvgLineTo(nvg, pos.x, pos.y + borderHeight); - nvgStroke(nvg); + nanovg::nvgStrokeWidth(nvg, 1.5f); + nanovg::nvgLineStyle(nvg, NVG_LINE_DASHED); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, pos.x + borderWidth, pos.y + borderHeight); + nanovg::nvgLineTo(nvg, pos.x + borderWidth, pos.y); + nanovg::nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, pos.x + borderWidth, pos.y + borderHeight); + nanovg::nvgLineTo(nvg, pos.x, pos.y + borderHeight); + nanovg::nvgStroke(nvg); canvasBorderResizer->render(nvg); } @@ -665,31 +680,31 @@ void Canvas::performRender(NVGcontext* nvg, Rectangle invalidRegion) NVGScopedState scopedState(nvg); // background colour to crop outside of border area - nvgBeginPath(nvg); - nvgRect(nvg, 0, 0, infiniteCanvasSize, infiniteCanvasSize); - nvgPathWinding(nvg, NVG_HOLE); - nvgRoundedRect(nvg, pos.getX(), pos.getY(), borderWidth, borderHeight, windowCorner); - nvgFillColor(nvg, nvgColour(PlugDataColours::presentationBackgroundColour)); - nvgFill(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRect(nvg, 0, 0, infiniteCanvasSize, infiniteCanvasSize); + nanovg::nvgPathWinding(nvg, NVG_HOLE); + nanovg::nvgRoundedRect(nvg, pos.getX(), pos.getY(), borderWidth, borderHeight, windowCorner); + nanovg::nvgFillColor(nvg, nvgColour(PlugDataColours::presentationBackgroundColour)); + nanovg::nvgFill(nvg); // background drop shadow to simulate a virtual plugin - nvgBeginPath(nvg); - nvgRect(nvg, 0, 0, infiniteCanvasSize, infiniteCanvasSize); - nvgPathWinding(nvg, NVG_HOLE); - nvgRoundedRect(nvg, pos.getX(), pos.getY(), borderWidth, borderHeight, windowCorner); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRect(nvg, 0, 0, infiniteCanvasSize, infiniteCanvasSize); + nanovg::nvgPathWinding(nvg, NVG_HOLE); + nanovg::nvgRoundedRect(nvg, pos.getX(), pos.getY(), borderWidth, borderHeight, windowCorner); int const shadowSize = 24 / scale; auto borderArea = Rectangle(0, 0, borderWidth, borderHeight).expanded(shadowSize); if (presentationShadowImage.needsUpdate(borderArea.getWidth(), borderArea.getHeight())) { presentationShadowImage = NVGImage(nvg, borderArea.getWidth(), borderArea.getHeight(), [borderArea, shadowSize, windowCorner](Graphics& g) { StackShadow::drawShadowForRect(g, borderArea.reduced(shadowSize).withPosition(shadowSize, shadowSize), shadowSize, windowCorner, 0.3f, 2); }, NVGImage::AlphaImage); } - auto const shadowImage = nvgImageAlphaPattern(nvg, pos.getX() - shadowSize, pos.getY() - shadowSize, borderArea.getWidth(), borderArea.getHeight(), 0, presentationShadowImage.getImageId(), nvgColour(Colours::black)); + auto const shadowImage = nanovg::nvgImageAlphaPattern(nvg, pos.getX() - shadowSize, pos.getY() - shadowSize, borderArea.getWidth(), borderArea.getHeight(), 0, presentationShadowImage.getImageId(), nvgColour(Colours::black)); - nvgStrokeColor(nvg, nvgColour(PlugDataColours::presentationBackgroundColour.contrasting(0.3f))); - nvgStrokeWidth(nvg, 0.5f / scale); - nvgFillPaint(nvg, shadowImage); - nvgFill(nvg); - nvgStroke(nvg); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::presentationBackgroundColour.contrasting(0.3f))); + nanovg::nvgStrokeWidth(nvg, 0.5f / scale); + nanovg::nvgFillPaint(nvg, shadowImage); + nanovg::nvgFill(nvg); + nanovg::nvgStroke(nvg); } } // render connections infront or behind objects depending on lock mode or overlay setting @@ -710,7 +725,7 @@ void Canvas::performRender(NVGcontext* nvg, Rectangle invalidRegion) if (graphArea) { NVGScopedState scopedState(nvg); - nvgTranslate(nvg, graphArea->getX(), graphArea->getY()); + nanovg::nvgTranslate(nvg, graphArea->getX(), graphArea->getY()); graphArea->render(nvg); } @@ -719,7 +734,7 @@ void Canvas::performRender(NVGcontext* nvg, Rectangle invalidRegion) if (viewport && lasso.isVisible() && !lasso.getBounds().isEmpty()) { auto lassoBounds = lasso.getBounds(); lassoBounds = lassoBounds.withSize(jmax(lasso.getWidth(), 2), jmax(lasso.getHeight(), 2)); - nvgDrawRoundedRect(nvg, lassoBounds.getX(), lassoBounds.getY(), lassoBounds.getWidth(), lassoBounds.getHeight(), nvgColour(PlugDataColours::objectSelectedOutlineColour.withAlpha(0.075f)), nvgColour(PlugDataColours::canvasBackgroundColour.interpolatedWith(PlugDataColours::objectSelectedOutlineColour, 0.65f)), 0.0f); + nanovg::nvgDrawRoundedRect(nvg, lassoBounds.getX(), lassoBounds.getY(), lassoBounds.getWidth(), lassoBounds.getHeight(), nvgColour(PlugDataColours::objectSelectedOutlineColour.withAlpha(0.075f)), nvgColour(PlugDataColours::canvasBackgroundColour.interpolatedWith(PlugDataColours::objectSelectedOutlineColour, 0.65f)), 0.0f); } suggestor->renderAutocompletion(nvg); @@ -739,7 +754,7 @@ void Canvas::performRender(NVGcontext* nvg, Rectangle invalidRegion) if (objectsDistributeResizer) objectsDistributeResizer->render(nvg); - nvgRestore(nvg); + nanovg::nvgRestore(nvg); // Draw scrollbars if (viewport) { @@ -754,7 +769,7 @@ void Canvas::renderAllObjects(NVGcontext* nvg, Rectangle const area) auto b = obj->getBounds(); if (b.intersects(area) && obj->isVisible()) { NVGScopedState scopedState(nvg); - nvgTranslate(nvg, b.getX(), b.getY()); + nanovg::nvgTranslate(nvg, b.getX(), b.getY()); obj->render(nvg); } } @@ -1590,8 +1605,10 @@ void Canvas::deselectAll(bool const broadcastChange) s->hideParameters(); if (!broadcastChange) { - // Add back the listener, but make sure it's added back 'after' the last event on the message queue - MessageManager::callAsync([this] { selectedComponents.addChangeListener(this); }); + MessageManager::callAsync([_this = SafePointer(this)] { + if (_this) + _this->selectedComponents.addChangeListener(_this); + }); } } @@ -2559,8 +2576,10 @@ void Canvas::setSelected(Component* component, bool const shouldNowBeSelected, b } if (!broadcastChange) { - // Add back the listener, but make sure it's added back 'after' the last event on the message queue - MessageManager::callAsync([this] { selectedComponents.addChangeListener(this); }); + MessageManager::callAsync([_this = SafePointer(this)] { + if (_this) + _this->selectedComponents.addChangeListener(_this); + }); } } diff --git a/Source/Canvas.h b/Source/Canvas.h index f87dac353d..016e00c729 100644 --- a/Source/Canvas.h +++ b/Source/Canvas.h @@ -6,13 +6,10 @@ #pragma once -#include +#include #ifdef NANOVG_GL_IMPLEMENTATION # include using namespace juce::gl; -# undef NANOVG_GL_IMPLEMENTATION -# include -# define NANOVG_GL_IMPLEMENTATION 1 #endif #include "ObjectGrid.h" // move to impl @@ -95,7 +92,9 @@ class Canvas final : public Component void focusLost(FocusChangeType cause) override; void updateFramebuffers(NVGcontext* nvg) override; + void render(NVGcontext* nvg) override; void performRender(NVGcontext* nvg, Rectangle invalidRegion); + void renderResizeHandle(NVGcontext* nvg, NVGcolor colour); void resized() override; @@ -251,8 +250,6 @@ class Canvas final : public Component int lastObjectGridSize = -1; - NVGImage dotsLargeImage; - Point const canvasOrigin; std::unique_ptr graphArea; @@ -273,8 +270,9 @@ class Canvas final : public Component Component objectLayer; Component connectionLayer; - NVGImage resizeHandleImage; + NVGCachedPath resizeHandlePath; NVGImage presentationShadowImage; + NVGFramebuffer dotsLargeImage; private: void changeListenerCallback(ChangeBroadcaster* c) override; diff --git a/Source/CanvasViewport.h b/Source/CanvasViewport.h index 4637a52232..070a454459 100644 --- a/Source/CanvasViewport.h +++ b/Source/CanvasViewport.h @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include "Object.h" @@ -109,7 +109,7 @@ class CanvasViewport : public Component if (approximatelyEqual(minimapAlpha, 0.0f)) return; - nvgGlobalAlpha(nvg, minimapAlpha); + nanovg::nvgGlobalAlpha(nvg, minimapAlpha); auto map = getMapBounds(); @@ -120,20 +120,20 @@ class CanvasViewport : public Component auto const mapBackground = canvasBackground.contrasting(0.5f); // draw background - nvgFillColor(nvg, nvgColour(mapBackground.withAlpha(0.4f))); - nvgFillRoundedRect(nvg, x - 4, y - 4, width + 8, height + 8, Corners::largeCornerRadius); + nanovg::nvgFillColor(nvg, nvgColour(mapBackground.withAlpha(0.4f))); + nanovg::nvgFillRoundedRect(nvg, x - 4, y - 4, width + 8, height + 8, Corners::largeCornerRadius); - nvgFillColor(nvg, nvgColour(mapBackground.withAlpha(0.8f))); + nanovg::nvgFillColor(nvg, nvgColour(mapBackground.withAlpha(0.8f))); // draw objects for (auto const* object : cnv->objects) { auto b = (object->getBounds().reduced(Object::margin).translated(map.offsetX, map.offsetY) - cnv->canvasOrigin).toFloat() * map.scale; - nvgFillRoundedRect(nvg, x + b.getX(), y + b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius * map.scale); + nanovg::nvgFillRoundedRect(nvg, x + b.getX(), y + b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius * map.scale); } // draw visible area - nvgDrawRoundedRect(nvg, x + (map.offsetX + map.viewBounds.getX() - cnv->canvasOrigin.x) * map.scale, y + (map.offsetY + map.viewBounds.getY() - cnv->canvasOrigin.y) * map.scale, map.viewBounds.getWidth() * map.scale, map.viewBounds.getHeight() * map.scale, nvgColour(canvasBackground.withAlpha(0.6f)), nvgColour(canvasBackground.contrasting(0.4f)), 0.0f); - nvgGlobalAlpha(nvg, 1.0f); + nanovg::nvgDrawRoundedRect(nvg, x + (map.offsetX + map.viewBounds.getX() - cnv->canvasOrigin.x) * map.scale, y + (map.offsetY + map.viewBounds.getY() - cnv->canvasOrigin.y) * map.scale, map.viewBounds.getWidth() * map.scale, map.viewBounds.getHeight() * map.scale, nvgColour(canvasBackground.withAlpha(0.6f)), nvgColour(canvasBackground.contrasting(0.4f)), 0.0f); + nanovg::nvgGlobalAlpha(nvg, 1.0f); } void mouseEnter(MouseEvent const& e) override @@ -395,10 +395,9 @@ class CanvasViewport : public Component void lookAndFeelChanged() override { - auto const scrollbarColour = findColour(ScrollBar::ColourIds::thumbColourId); - scrollbarCol = nvgColour(scrollbarColour); - activeScrollbarCol = nvgColour(scrollbarColour.interpolatedWith(PlugDataColours::canvasBackgroundColour.contrasting(0.6f), 0.7f)); - scrollbarBgCol = nvgColour(scrollbarColour.interpolatedWith(PlugDataColours::canvasBackgroundColour, 0.7f)); + scrollbarCol = nvgColour(PlugDataColours::scrollbarThumbColour); + activeScrollbarCol = nvgColour(PlugDataColours::scrollbarThumbColour.interpolatedWith(PlugDataColours::canvasBackgroundColour.contrasting(0.6f), 0.7f)); + scrollbarBgCol = nvgColour(PlugDataColours::scrollbarThumbColour.interpolatedWith(PlugDataColours::canvasBackgroundColour, 0.7f)); repaint(); } @@ -490,10 +489,10 @@ class CanvasViewport : public Component } scrollbarBgCol.a = (1.0f - growAnimation) * 150; - nvgDrawRoundedRect(nvg, fullBounds.getX(), fullBounds.getY(), fullBounds.getWidth(), fullBounds.getHeight(), scrollbarBgCol, scrollbarBgCol, thumbCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, fullBounds.getX(), fullBounds.getY(), fullBounds.getWidth(), fullBounds.getHeight(), scrollbarBgCol, scrollbarBgCol, thumbCornerRadius); auto const scrollBarThumbCol = isMouseDragging ? activeScrollbarCol : scrollbarCol; - nvgDrawRoundedRect(nvg, growingBounds.getX(), growingBounds.getY(), growingBounds.getWidth(), growingBounds.getHeight(), scrollBarThumbCol, scrollBarThumbCol, thumbCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, growingBounds.getX(), growingBounds.getY(), growingBounds.getWidth(), growingBounds.getHeight(), scrollBarThumbCol, scrollBarThumbCol, thumbCornerRadius); } NVGcolor scrollbarCol; @@ -553,24 +552,29 @@ class CanvasViewport : public Component updater.addAnimator(moveAnimator); updater.addAnimator(bounceAnimator); - setCachedComponentImage(new NVGSurface::InvalidationListener(editor->nvgSurface, this, [this] { + setCachedComponentImage(new NVGSurface::InvalidationListener(editor->nvgSurface, this, true, [this] { return editor->getTabComponent().getVisibleCanvases().contains(this->cnv); })); } + void render(NVGcontext* nvg) override + { + cnv->performRender(nvg, getLocalBounds()); + } + void render(NVGcontext* nvg, Rectangle const area) { minimap.render(nvg); if (area.intersects(vbar.getBounds())) { NVGScopedState scopedState(nvg); - nvgTranslate(nvg, vbar.getX(), vbar.getY()); + nanovg::nvgTranslate(nvg, vbar.getX(), vbar.getY()); vbar.render(nvg); } if (area.intersects(hbar.getBounds())) { NVGScopedState scopedState(nvg); - nvgTranslate(nvg, hbar.getX(), hbar.getY()); + nanovg::nvgTranslate(nvg, hbar.getX(), hbar.getY()); hbar.render(nvg); } } diff --git a/Source/Components/Buttons.cpp b/Source/Components/Buttons.cpp index 106a7b97a4..d0c18daa97 100644 --- a/Source/Components/Buttons.cpp +++ b/Source/Components/Buttons.cpp @@ -101,19 +101,25 @@ void SmallIconButton::mouseExit(MouseEvent const& e) repaint(); } +void SmallIconButton::setIconFontSize(int newSize) +{ + iconSize = newSize; + repaint(); +} + void SmallIconButton::paint(Graphics& g) { auto colour = PlugDataColours::toolbarTextColour; if (!isEnabled()) { - colour = Colours::grey; + colour = colour.withAlpha(0.35f); } else if (getToggleState()) { colour = PlugDataColours::toolbarActiveColour; } else if (isMouseOver()) { colour = PlugDataColours::toolbarTextColour.brighter(0.8f); } - Fonts::drawIcon(g, getButtonText(), getLocalBounds(), colour, 12); + Fonts::drawIcon(g, getButtonText(), getLocalBounds(), colour, iconSize); } WidePanelButton::WidePanelButton(String const& icon, int const iconSize) diff --git a/Source/Components/Buttons.h b/Source/Components/Buttons.h index 1042be4516..5d20a83f12 100644 --- a/Source/Components/Buttons.h +++ b/Source/Components/Buttons.h @@ -36,6 +36,12 @@ class SmallIconButton : public TextButton { void mouseExit(MouseEvent const& e) override; void paint(Graphics& g) override; + + int iconSize = 12; + +public: + void setIconFontSize(int newSize); + }; class WidePanelButton final : public TextButton { diff --git a/Source/Components/CanvasBorderResizer.h b/Source/Components/CanvasBorderResizer.h index 411b13db80..ffb2eb0d2b 100644 --- a/Source/Components/CanvasBorderResizer.h +++ b/Source/Components/CanvasBorderResizer.h @@ -70,11 +70,11 @@ class BorderResizer final : public Component if (getValue(cnv->locked)) return; NVGScopedState state(nvg); - nvgSave(nvg); - nvgTranslate(nvg, getX(), getY()); + nanovg::nvgSave(nvg); + nanovg::nvgTranslate(nvg, getX(), getY()); auto const bounds = getLocalBounds().reduced(isMouseOver() ? 0 : 2); - nvgDrawRoundedRect(nvg, bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), nvgColour(PlugDataColours::canvasDotsColour), nvgColour(PlugDataColours::canvasDotsColour), bounds.getWidth() / 2.0f); - nvgRestore(nvg); + nanovg::nvgDrawRoundedRect(nvg, bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), nvgColour(PlugDataColours::canvasDotsColour), nvgColour(PlugDataColours::canvasDotsColour), bounds.getWidth() / 2.0f); + nanovg::nvgRestore(nvg); } void valueChanged(Value& v) override diff --git a/Source/Components/CanvasSearchHighlight.h b/Source/Components/CanvasSearchHighlight.h index 7b0e056ce5..a311a26fca 100644 --- a/Source/Components/CanvasSearchHighlight.h +++ b/Source/Components/CanvasSearchHighlight.h @@ -41,7 +41,7 @@ class CanvasSearchHighlight final : public Component NVGcolor iCol = oCol = nvgColour(PlugDataColours::objectSelectedOutlineColour); iCol.a = opacity > 1.0f ? 150 : 150 * opacity; oCol.a = opacity > 1.0f ? 255 : 255 * opacity; - nvgDrawRoundedRect(nvg, oB.getX(), oB.getY(), oB.getWidth(), oB.getHeight(), iCol, oCol, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, oB.getX(), oB.getY(), oB.getWidth(), oB.getHeight(), iCol, oCol, Corners::objectCornerRadius); } void chainAnimation(Animator const& previousAnimation) diff --git a/Source/Components/ColourPicker.h b/Source/Components/ColourPicker.h index bcd1cbfae3..6d4ebf0f56 100644 --- a/Source/Components/ColourPicker.h +++ b/Source/Components/ColourPicker.h @@ -5,8 +5,10 @@ */ #pragma once -// Eyedropper will create a snapshot of the top level component, -// to allow the user to pick colours from anywhere in the app +#include "PluginEditor.h" + +// Eyedropper samples the editor's rendered framebuffer, so it can pick colours +// from anywhere in the (GPU-rendered) app UI class Eyedropper final : public Timer , public MouseListener { class EyedropperDisplayComponnent final : public Component { @@ -132,7 +134,8 @@ class Eyedropper final : public Timer callback = [](Colour) { }; colourDisplayer.hide(); stopTimer(); - topLevel->removeMouseListener(this); + if (topLevel) + topLevel->removeMouseListener(this); topLevel = nullptr; } @@ -144,33 +147,46 @@ class Eyedropper final : public Timer void timerCallback() override { - auto const position = topLevel->getMouseXYRelative(); - auto const surfaceMousePosition = editor->nvgSurface.getLocalPoint(topLevel, position); - auto const mouseOverSurface = editor->nvgSurface.getLocalBounds().contains(surfaceMousePosition); - - if (mouseOverSurface) { - editor->nvgSurface.setRenderThroughImage(true); - editor->nvgSurface.render(); - } - - componentImage = topLevel->createComponentSnapshot(topLevel->getLocalBounds(), false, 1.0f); - colourDisplayer.setCentrePosition(topLevel->localPointToGlobal(position)); - colourDisplayer.setROI(componentImage, position); - setColour(componentImage.getPixelAt(position.x, position.y)); - - if (mouseOverSurface) { - editor->nvgSurface.setRenderThroughImage(false); - } + if (!topLevel) { + stopTimer(); + return; + } + + auto const positionInTopLevel = topLevel->getMouseXYRelative(); + colourDisplayer.setCentrePosition(topLevel->localPointToGlobal(positionInTopLevel)); + + if (!editor) + return; + + // The whole editor is rendered through OpenGL/Metal, so an ordinary + // component snapshot comes back empty. Sample the rendered framebuffer + // instead. We only read a small region around the cursor to keep the + // per-tick GPU read-back cheap. + auto const positionInEditor = editor->getLocalPoint(topLevel, positionInTopLevel); + + constexpr int regionSize = 48; + auto const region = Rectangle(0, 0, regionSize, regionSize) + .withCentre(positionInEditor) + .getIntersection(editor->getLocalBounds()); + if (region.isEmpty()) + return; + + auto const image = editor->nvgSurface.renderToImage(region); + if (image.isNull()) + return; + + auto const positionInImage = positionInEditor - region.getTopLeft(); + colourDisplayer.setROI(image, positionInImage); + setColour(image.getPixelAt(positionInImage.x, positionInImage.y)); } std::function callback; int timerCount = 0; - Component* topLevel = nullptr; + Component::SafePointer topLevel = nullptr; EyedropperDisplayComponnent colourDisplayer; - Image componentImage; Colour currentColour; - PluginEditor* editor; + PluginEditor* editor = nullptr; }; class ColourPicker final : public Component @@ -223,6 +239,7 @@ class ColourPicker final : public Component _topLevelComponent = topLevelComponent; setCurrentColour(currentColour); + lookAndFeelChanged(); // we need to put the selector into a holder, as launchAsynchronously will delete the component when its done auto selectorHolder = std::make_unique(this); @@ -295,8 +312,6 @@ class ColourPicker final : public Component update(dontSendNotification); updateMode(); - - lookAndFeelChanged(); } ~ColourPicker() override diff --git a/Source/Components/ConsoleMessageDisplay.h b/Source/Components/ConsoleMessageDisplay.h deleted file mode 100644 index 92808b8661..0000000000 --- a/Source/Components/ConsoleMessageDisplay.h +++ /dev/null @@ -1,102 +0,0 @@ -/* - // Copyright (c) 2026 Timothy Schoen - // For information on usage and redistribution, and for a DISCLAIMER OF ALL - // WARRANTIES, see the file, "LICENSE.txt," in this distribution. -*/ -#pragma once - -#pragma once - -class ConsoleMessageDisplay final : public Component { -public: - ConsoleMessageDisplay(PluginEditor* e) - : editor(e) - { - setInterceptsMouseClicks(false, false); - setVisible(false); - - setCachedComponentImage(new NVGSurface::InvalidationListener(e->nvgSurface, this)); - } - - void showMessage(SmallString const& message, bool const isWarning) - { - if (message.isEmpty()) { - hide(); - return; - } - - currentMessage = message.toString(); - currentIsWarning = isWarning; - - if (auto* parent = getParentComponent()) - parent->resized(); - repaint(); - - setVisible(true); - setBounds(getBounds().withWidth(getDesiredWidth()).withRight(getRight())); - - hideTimer.startTimer(2000); - } - - void hide() - { - setVisible(false); - currentMessage = { }; - if (auto* parent = getParentComponent()) - parent->resized(); - } - - int getDesiredWidth() const - { - if (currentMessage.isEmpty()) - return 0; - auto const attr = makeAttributedString(currentMessage); - TextLayout layout; - layout.createLayout(attr, 10000.0f); - return static_cast(std::ceil(layout.getWidth())) + 28; - } - - void paint(Graphics& g) override - { - auto const bounds = getLocalBounds(); - auto const textColour = currentIsWarning ? Colours::orange : PlugDataColours::toolbarTextColour; - - auto const b = getLocalBounds().reduced(5); - StackShadow::drawShadowForRect(g, b.reduced(3.0f), 10, Corners::largeCornerRadius, 0.4f, 1); - - g.setColour(PlugDataColours::toolbarBackgroundColour); - g.fillRoundedRectangle(b.toFloat(), Corners::largeCornerRadius); - - g.setColour(PlugDataColours::toolbarOutlineColour); - g.drawRoundedRectangle(b.toFloat(), Corners::largeCornerRadius, 1.0f); - - AttributedString attr; - attr.setJustification(Justification::centred); - attr.append(currentMessage, - Fonts::getDefaultFont().withHeight(13.0f), - textColour); - attr.draw(g, bounds.reduced(5).toFloat().reduced(10, 0)); - } - - AttributedString makeAttributedString(String const& text) const - { - AttributedString attr; - attr.setJustification(Justification::centredLeft); - auto const textColour = currentIsWarning ? Colours::orange : PlugDataColours::toolbarTextColour; - attr.append(text, Fonts::getDefaultFont().withHeight(13.0f), textColour); - return attr; - } - - PluginEditor* editor; - String currentMessage; - bool currentIsWarning = false; - - TimedCallback hideTimer = TimedCallback([this] { - hideTimer.stopTimer(); - hide(); - }); - - NVGImage componentImage; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ConsoleMessageDisplay) -}; diff --git a/Source/Components/DraggableNumber.cpp b/Source/Components/DraggableNumber.cpp index 50d1e75c1a..818daa2f11 100644 --- a/Source/Components/DraggableNumber.cpp +++ b/Source/Components/DraggableNumber.cpp @@ -446,21 +446,21 @@ int DraggableNumber::getDecimalAtPosition(int const x, Rectangle* positio return draggedDecimal; } + void DraggableNumber::render(NVGcontext* nvg, NVGGraphicsContext* llgc) { NVGScopedState scopedState(nvg); - nvgIntersectScissor(nvg, 0, 0, getWidth(), getHeight()); + nanovg::nvgIntersectScissor(nvg, 0, 0, getWidth(), getHeight()); if (editor) { - Graphics g(*llgc); - paintEntireComponent(g, true); + llgc->renderComponent(*this); return; } if (hoveredDecimal >= 0) { auto const highlightColour = outlineColour.withAlpha(isMouseButtonDown() ? 0.5f : 0.3f); - nvgFillColor(nvg, nvgColour(highlightColour)); - nvgFillRoundedRect(nvg, hoveredDecimalPosition.getX(), hoveredDecimalPosition.getY(), hoveredDecimalPosition.getWidth(), hoveredDecimalPosition.getHeight(), 2.5f); + nanovg::nvgFillColor(nvg, nvgColour(highlightColour)); + nanovg::nvgFillRoundedRect(nvg, hoveredDecimalPosition.getX(), hoveredDecimalPosition.getY(), hoveredDecimalPosition.getWidth(), hoveredDecimalPosition.getHeight(), 2.5f); } auto textArea = border.subtractedFrom(getLocalBounds()).toDouble(); @@ -484,10 +484,6 @@ void DraggableNumber::render(NVGcontext* nvg, NVGGraphicsContext* llgc) } } - nvgFontFace(nvg, "Inter-Tabular"); - nvgFontSize(nvg, font.getHeightInPoints()); - nvgTextAlign(nvg, NVG_ALIGN_MIDDLE | NVG_ALIGN_LEFT); - nvgFillColor(nvg, nvgColour(textColour)); // NOTE: We could simply do `String(numberText.getDoubleValue(), 0)` but using string manipulation // bypasses any potential issues with precision @@ -509,12 +505,16 @@ void DraggableNumber::render(NVGcontext* nvg, NVGGraphicsContext* llgc) // Only display the decimal point if fractional exists, but make sure to show it as a user hovers over the fractional decimal places auto const formatedNumber = isMouseOverOrDragging() && hoveredDecimal > 0 ? numberText : removeDecimalNumString(numberText); - nvgText(nvg, textArea.getX(), textArea.getCentreY(), formatedNumber.toRawUTF8(), nullptr); + NVGGraphicsContext::ScopedAnchoredDraw anchor(*llgc, getLocalBounds().toFloat()); + llgc->setFill(textColour); + llgc->setFont(font); + llgc->drawText(formatedNumber, textArea.toFloat()); if (dragMode == Regular) { textArea = textArea.withTrimmedLeft(numberTextLength); - nvgFillColor(nvg, nvgColour(textColour.withAlpha(0.4f))); - nvgText(nvg, textArea.getX(), textArea.getCentreY(), extraNumberText.toRawUTF8(), nullptr); + + llgc->setFill(textColour.withAlpha(0.4f)); + llgc->drawText(extraNumberText, textArea.toFloat()); } } @@ -811,6 +811,8 @@ void DraggableListNumber::mouseUp(MouseEvent const& e) dragEnd(); } + + void DraggableListNumber::paint(Graphics& g) { if (hoveredDecimal >= 0) { @@ -830,28 +832,25 @@ void DraggableListNumber::paint(Graphics& g) void DraggableListNumber::render(NVGcontext* nvg, NVGGraphicsContext* llgc) { NVGScopedState scopedState(nvg); - nvgIntersectScissor(nvg, 0.5f, 0.5f, getWidth() - 1, getHeight() - 1); + nanovg::nvgIntersectScissor(nvg, 0.5f, 0.5f, getWidth() - 1, getHeight() - 1); if (editor) { - Graphics g(*llgc); - paintEntireComponent(g, true); + llgc->renderComponent(*this); return; } if (hoveredDecimal >= 0) { auto const highlightColour = outlineColour.withAlpha(isMouseButtonDown() ? 0.5f : 0.3f); - nvgFillColor(nvg, nvgColour(highlightColour)); - nvgFillRoundedRect(nvg, hoveredDecimalPosition.getX(), hoveredDecimalPosition.getY() - 1, hoveredDecimalPosition.getWidth(), hoveredDecimalPosition.getHeight(), 2.5f); + nanovg::nvgFillColor(nvg, nvgColour(highlightColour)); + nanovg::nvgFillRoundedRect(nvg, hoveredDecimalPosition.getX(), hoveredDecimalPosition.getY() - 1, hoveredDecimalPosition.getWidth(), hoveredDecimalPosition.getHeight(), 2.5f); } - nvgFontFace(nvg, "Inter-Tabular"); - nvgFontSize(nvg, font.getHeightInPoints()); - nvgTextAlign(nvg, NVG_ALIGN_MIDDLE | NVG_ALIGN_LEFT); - nvgFillColor(nvg, nvgColour(textColour)); - - auto const listText = currentValue; auto const textArea = border.subtractedFrom(getBounds()); - nvgText(nvg, textArea.getX(), textArea.getCentreY() + 1.5f, listText.toRawUTF8(), nullptr); + + NVGGraphicsContext::ScopedAnchoredDraw anchor(*llgc, getLocalBounds().toFloat()); + llgc->setFill(textColour); + llgc->setFont(font); + llgc->drawText(currentValue, textArea.toFloat()); } void DraggableListNumber::textEditorReturnKeyPressed(TextEditor& editor) diff --git a/Source/Components/GraphArea.h b/Source/Components/GraphArea.h index b0673697ff..4e16dbc6e7 100644 --- a/Source/Components/GraphArea.h +++ b/Source/Components/GraphArea.h @@ -56,10 +56,9 @@ class GraphArea final : public Component { auto lineBounds = getLocalBounds().toFloat().reduced(4.0f); - nvgDrawRoundedRect(nvg, lineBounds.getX(), lineBounds.getY(), lineBounds.getWidth(), lineBounds.getHeight(), nvgRGBA(0, 0, 0, 0), nvgColour(PlugDataColours::graphAreaColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, lineBounds.getX(), lineBounds.getY(), lineBounds.getWidth(), lineBounds.getHeight(), nanovg::nvgRGBA(0, 0, 0, 0), nvgColour(PlugDataColours::graphAreaColour), Corners::objectCornerRadius); if (!getValue(canvas->locked)) { - auto& resizeHandleImage = canvas->resizeHandleImage; int angle = 360; auto getVert = [lineBounds](int const index) -> Point { @@ -80,14 +79,11 @@ class GraphArea final : public Component for (int i = 0; i < 4; i++) { NVGScopedState scopedState(nvg); // Rotate around centre - nvgTranslate(nvg, getVert(i).x, getVert(i).y); - nvgRotate(nvg, degreesToRadians(angle)); - nvgTranslate(nvg, -3.0f, -3.0f); - - nvgBeginPath(nvg); - nvgRect(nvg, 0, 0, 9, 9); - nvgFillPaint(nvg, nvgImageAlphaPattern(nvg, 0, 0, 9, 9, 0, resizeHandleImage.getImageId(), nvgColour(PlugDataColours::graphAreaColour))); - nvgFill(nvg); + nanovg::nvgTranslate(nvg, getVert(i).x, getVert(i).y); + nanovg::nvgRotate(nvg, degreesToRadians(angle)); + nanovg::nvgTranslate(nvg, -3.0f, -3.0f); + + canvas->renderResizeHandle(nvg, nvgColour(PlugDataColours::graphAreaColour)); angle -= 90; } } diff --git a/Source/Components/MarkupDisplay.h b/Source/Components/MarkupDisplay.h index 8385f30872..9e69395f8f 100644 --- a/Source/Components/MarkupDisplay.h +++ b/Source/Components/MarkupDisplay.h @@ -346,7 +346,7 @@ class Block : public Component { font = Fonts::getBoldFont().withHeight(15); } if (italic) { - font = Fonts::getVariableFont().italicised().withHeight(15); + font = Fonts::getCurrentFont().italicised().withHeight(15); } } } @@ -630,10 +630,10 @@ class TableBlock final : public Block { for (int i = 0; i < table.cells.size(); i++) { OwnedArray const* row = table.cells[i]; for (int j = 0; j < row->size(); j++) { - if (j < table.columnwidths.size()) { + if (j < static_cast(table.columnwidths.size())) { table.columnwidths[j] = jmax(table.columnwidths[j], (*row)[j]->width); } else { - table.columnwidths[j] = (*row)[j]->width; + table.columnwidths.add((*row)[j]->width); } } } @@ -645,7 +645,7 @@ class TableBlock final : public Block { for (int j = 0; j < row->size(); j++) { rowheight = jmax(rowheight, (*row)[j]->height); } - table.rowheights[i] = rowheight; + table.rowheights.add(rowheight); } table.setBounds(0, 0, getWidthRequired() + table.leftmargin + table.cellgap, getHeightRequired(0.f)); } diff --git a/Source/Components/PropertiesPanel.cpp b/Source/Components/PropertiesPanel.cpp index 983739cfc4..7f4116d257 100644 --- a/Source/Components/PropertiesPanel.cpp +++ b/Source/Components/PropertiesPanel.cpp @@ -428,7 +428,7 @@ PropertiesPanel::InspectorColourComponent::InspectorColourComponent(String const editor->setBorder(BorderSize(0, 0, 4, 1)); editor->setJustification(Justification::centred); editor->setInputRestrictions(allowAlpha ? 9 : 7, "#0123456789ABCDEFabcdef"); - editor->applyColourToAllText(Colour::fromString(currentColour.toString()).contrasting(0.95f)); + editor->applyColourToAllText(getValue(currentColour).contrasting(0.95f)); }; hexValueEditor.onEditorHide = [this] { @@ -437,7 +437,7 @@ PropertiesPanel::InspectorColourComponent::InspectorColourComponent(String const } else { colour = String("ff") + hexValueEditor.getText().substring(1).toLowerCase(); } - currentColour.setValue(colour); + currentColour.setValue(colourToVar(Colour::fromString(colour.toString()))); }; hexValueEditor.onTextChange = [this] { @@ -467,8 +467,8 @@ PropertiesPanelProperty* PropertiesPanel::InspectorColourComponent::createCopy() void PropertiesPanel::InspectorColourComponent::updateHexValue() { - hexValueEditor.setColour(Label::textColourId, Colour::fromString(currentColour.toString()).contrasting(0.95f)); - hexValueEditor.setText(String("#") + currentColour.toString().substring(2).toUpperCase(), dontSendNotification); + hexValueEditor.setColour(Label::textColourId, getValue(currentColour).contrasting(0.95f)); + hexValueEditor.setText(String("#") + getValue(currentColour).toString().substring(2).toUpperCase(), dontSendNotification); } void PropertiesPanel::InspectorColourComponent::resized() @@ -483,7 +483,7 @@ void PropertiesPanel::InspectorColourComponent::resized() void PropertiesPanel::InspectorColourComponent::paint(Graphics& g) { - auto const colour = Colour::fromString(currentColour.toString()); + auto const colour = getValue(currentColour); auto const hoverColour = isMouseOver ? colour.brighter(0.4f) : colour; auto swatchBounds = getLocalBounds().removeFromRight(getWidth() / 2).toFloat().reduced(4.5f); @@ -520,11 +520,11 @@ void PropertiesPanel::InspectorColourComponent::mouseDown(MouseEvent const& e) if (e.x > getWidth() - 28) { auto const pickerBounds = getScreenBounds().withTrimmedLeft(getWidth() / 2).expanded(5); - ColourPicker::getInstance()->show(findParentComponentOfClass(), getTopLevelComponent(), false, Colour::fromString(currentColour.toString()), pickerBounds, [_this = SafePointer(this)](Colour const c) { + ColourPicker::getInstance()->show(findParentComponentOfClass(), getTopLevelComponent(), false, getValue(currentColour), pickerBounds, [_this = SafePointer(this)](Colour const c) { if (!_this) return; - _this->currentColour = c.toString(); + _this->currentColour = colourToVar(c); _this->repaint(); }); } else { @@ -556,7 +556,7 @@ class SwatchComponent final : public Component { void paint(Graphics& g) override { - auto const colour = Colour::fromString(colourValue.toString()); + auto const colour = getValue(colourValue); g.setColour(isMouseOver() ? colour.brighter(0.4f) : colour); g.fillEllipse(getLocalBounds().reduced(1).toFloat()); @@ -577,11 +577,11 @@ class SwatchComponent final : public Component { void mouseDown(MouseEvent const& e) override { auto const pickerBounds = getScreenBounds().expanded(5); - ColourPicker::getInstance()->show(findParentComponentOfClass(), getTopLevelComponent(), false, Colour::fromString(colourValue.toString()), pickerBounds, [_this = SafePointer(this)](Colour const c) { + ColourPicker::getInstance()->show(findParentComponentOfClass(), getTopLevelComponent(), false, getValue(colourValue), pickerBounds, [_this = SafePointer(this)](Colour const c) { if (!_this) return; - _this->colourValue = c.toString(); + _this->colourValue = colourToVar(c); _this->repaint(); }); } @@ -614,7 +614,7 @@ PropertiesPanel::ColourComponent::ColourComponent(String const& propertyName, Va }; hexValueEditor.onFocusLost = [this] { - currentColour.setValue(colour); + currentColour.setValue(colourToVar(Colour::fromString(colour.toString()))); }; addAndMakeVisible(*swatchComponent); @@ -643,7 +643,7 @@ PropertiesPanelProperty* PropertiesPanel::ColourComponent::createCopy() void PropertiesPanel::ColourComponent::updateHexValue() { - hexValueEditor.setText(String("#") + currentColour.toString().substring(2).toUpperCase()); + hexValueEditor.setText(String("#") + getValue(currentColour).toString().substring(2).toUpperCase()); } void PropertiesPanel::ColourComponent::resized() diff --git a/Source/Components/SuggestionComponent.h b/Source/Components/SuggestionComponent.h index 1d44038e16..e2df826952 100644 --- a/Source/Components/SuggestionComponent.h +++ b/Source/Components/SuggestionComponent.h @@ -142,9 +142,8 @@ class AutoCompleteComponent final void render(NVGcontext* nvg) override { NVGScopedState scopedState(nvg); - nvgTranslate(nvg, getX(), getY()); - Graphics g(*cnv->editor->getNanoLLGC()); - paintEntireComponent(g, true); + nanovg::nvgTranslate(nvg, getX(), getY()); + cnv->editor->getNanoLLGC()->renderComponent(*this); } private: diff --git a/Source/Components/TouchSelectionHelper.h b/Source/Components/TouchSelectionHelper.h index 5cb61d350c..1491123374 100644 --- a/Source/Components/TouchSelectionHelper.h +++ b/Source/Components/TouchSelectionHelper.h @@ -13,20 +13,18 @@ #include "PluginEditor.h" #include "Objects/ObjectBase.h" -class TouchSelectionHelper final : public Component - , public NVGComponent { +class TouchSelectionHelper final : public Component { public: + static constexpr int buttonWidth = 48; + explicit TouchSelectionHelper(PluginEditor* e) - : NVGComponent(this) - , editor(e) + : editor(e) { - addAndMakeVisible(actionButtons.add(new MainToolbarButton(Icons::ExportState))); // This icon doubles as a "open" icon in the mobile app - addAndMakeVisible(actionButtons.add(new MainToolbarButton(Icons::Help))); - addAndMakeVisible(actionButtons.add(new MainToolbarButton(Icons::Trash))); - addAndMakeVisible(actionButtons.add(new MainToolbarButton(Icons::More))); - - updateCachedRenderingMode(); + addAndMakeVisible(actionButtons.add(new SmallIconButton(Icons::ExportState))); // This icon doubles as a "open" icon in the mobile app + addAndMakeVisible(actionButtons.add(new SmallIconButton(Icons::Help))); + addAndMakeVisible(actionButtons.add(new SmallIconButton(Icons::Trash))); + addAndMakeVisible(actionButtons.add(new SmallIconButton(Icons::More))); actionButtons[0]->onClick = [this] { auto* cnv = editor->getCurrentCanvas(); @@ -244,47 +242,26 @@ class TouchSelectionHelper final : public Component toFront(false); } + int getIdealWidth() const { return actionButtons.size() * buttonWidth; } + void resized() override { - updateCachedRenderingMode(); - - auto b = editor->usesFloatingPanels() ? getLocalBounds().reduced(5) : getLocalBounds(); + auto b = getLocalBounds(); for (auto* button : actionButtons) { - button->setBounds(b.removeFromLeft(48)); + button->setBounds(b.removeFromLeft(buttonWidth)); } } private: - void updateCachedRenderingMode() - { - bool const shouldUseCachedRendering = editor->usesFloatingPanels(); - if (cachedRenderingEnabled == shouldUseCachedRendering) - return; - - cachedRenderingEnabled = shouldUseCachedRendering; - setCachedComponentImage(shouldUseCachedRendering ? new NVGSurface::InvalidationListener(editor->nvgSurface, this) : nullptr); - } - void paint(Graphics& g) override { - bool const floatingPanels = editor->usesFloatingPanels(); - auto const b = floatingPanels ? getLocalBounds().reduced(5) : getLocalBounds(); + auto const b = getLocalBounds(); g.setColour(PlugDataColours::toolbarBackgroundColour); - if (floatingPanels) { - StackShadow::drawShadowForRect(g, b.reduced(3.0f), 10, Corners::largeCornerRadius, 0.4f, 1); - g.fillRoundedRectangle(b.toFloat(), Corners::largeCornerRadius); - } else { - g.fillRect(b); - } - - g.setColour(PlugDataColours::toolbarOutlineColour); - if (floatingPanels) - g.drawRoundedRectangle(b.toFloat(), Corners::largeCornerRadius, 1.0f); + g.fillRect(b); } PluginEditor* editor; - OwnedArray actionButtons; - bool cachedRenderingEnabled = false; + OwnedArray actionButtons; }; diff --git a/Source/Components/WelcomePanel.h b/Source/Components/WelcomePanel.h index 3848b496e7..6e714dfb81 100644 --- a/Source/Components/WelcomePanel.h +++ b/Source/Components/WelcomePanel.h @@ -6,8 +6,8 @@ #pragma once #include "Utility/Autosave.h" -#include "Utility/CachedTextRender.h" -#include "Utility/NVGGraphicsContext.h" +#include "Utility/Fonts.h" +#include "Utility/NVGUtils.h" #include "Components/BouncingViewport.h" #include "Utility/PatchInfo.h" @@ -27,26 +27,19 @@ class WelcomePanel final : public Component void paint(Graphics& g) override { - auto* nvg = dynamic_cast(g.getInternalContext()).getContext(); - if (panel.currentTab == Home && panel.searchQuery.isEmpty()) { if (panel.recentlyOpenedTiles.isEmpty()) { - nvgFontFace(nvg, "Inter-Bold"); - nvgFontSize(nvg, 34); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgFillColor(nvg, nvgColour(PlugDataColours::panelTextColour)); - nvgText(nvg, getWidth() / 2, getHeight() / 2 - 80, "Welcome to plugdata", nullptr); + g.setFont(Fonts::getBoldFont().withPointHeight(34.0f)); + g.setColour(PlugDataColours::panelTextColour); + g.drawText("Welcome to plugdata", Rectangle(0.0f, getHeight() * 0.5f - 104.0f, static_cast(getWidth()), 48.0f), Justification::centred, false); } else { - nvgFontFace(nvg, "Inter-Bold"); - nvgFontSize(nvg, 14); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgFillColor(nvg, nvgColour(PlugDataColours::panelTextColour)); - nvgText(nvg, 96, 138, "Recently Opened", nullptr); - - nvgFontFace(nvg, "icon_font-Regular"); - nvgFontSize(nvg, 14); - nvgFillColor(nvg, nvgColour(PlugDataColours::panelTextColour.withAlpha(isHoveringClearButton ? 0.6f : 1.0f))); - nvgText(nvg, clearButtonBounds.getCentreX(), clearButtonBounds.getCentreY(), Icons::Clear.toRawUTF8(), nullptr); + g.setFont(Fonts::getBoldFont().withPointHeight(14.0f)); + g.setColour(PlugDataColours::panelTextColour); + g.drawText("Recently Opened", Rectangle(0.0f, 128.0f, 192.0f, 20.0f), Justification::centred, false); + + g.setFont(Fonts::getIconFont().withPointHeight(14.0f)); + g.setColour(PlugDataColours::panelTextColour.withAlpha(isHoveringClearButton ? 0.6f : 1.0f)); + g.drawText(Icons::Clear, clearButtonBounds, Justification::centred, false); } } } @@ -104,7 +97,6 @@ class WelcomePanel final : public Component void paint(Graphics& g) override { auto const bounds = getLocalBounds().reduced(12); - auto* nvg = dynamic_cast(g.getInternalContext()).getContext(); auto const width = getWidth(); auto const height = getHeight(); @@ -115,75 +107,69 @@ class WelcomePanel final : public Component auto const lB = bounds.toFloat().expanded(0.5f); { - auto const bgCol = !isHovered ? nvgColour(PlugDataColours::panelForegroundColour) : nvgColour(PlugDataColours::toolbarBackgroundColour); + auto const bgColour = !isHovered ? PlugDataColours::panelForegroundColour : PlugDataColours::toolbarBackgroundColour; // Draw border around - nvgDrawRoundedRect(nvg, lB.getX(), lB.getY(), lB.getWidth(), lB.getHeight(), bgCol, nvgColour(PlugDataColours::toolbarOutlineColour), Corners::largeCornerRadius); + g.setColour(bgColour); + g.fillRoundedRectangle(lB, Corners::largeCornerRadius); + g.setColour(PlugDataColours::toolbarOutlineColour); + g.drawRoundedRectangle(lB, Corners::largeCornerRadius, 1.0f); } auto const bgColour = PlugDataColours::panelForegroundColour; - auto const bgCol = nvgColour(bgColour); - auto const newOpenIconCol = nvgColour(bgColour.contrasting().withAlpha(0.32f)); + auto const newOpenIconColour = bgColour.contrasting().withAlpha(0.32f); constexpr auto iconSize = 48; constexpr auto iconHalf = iconSize * 0.5f; - auto const circleBounds = Rectangle(lB.getX() + 40 - iconHalf, lB.getCentreY() - iconHalf, iconSize, iconSize); + auto const circleBounds = Rectangle(lB.getX() + 40.0f - iconHalf, lB.getCentreY() - iconHalf, iconSize, iconSize); // Background circle - nvgDrawRoundedRect(nvg, circleBounds.getX(), circleBounds.getY(), iconSize, iconSize, newOpenIconCol, newOpenIconCol, iconHalf); + g.setColour(newOpenIconColour); + g.fillRoundedRectangle(circleBounds, iconHalf); switch (type) { case New: { // Draw a cross icon manually - constexpr auto lineThickness = 4; + constexpr auto lineThickness = 4.0f; constexpr auto lineRad = lineThickness * 0.5f; - constexpr auto crossSize = 26; + constexpr auto crossSize = 26.0f; constexpr auto halfSize = crossSize * 0.5f; + g.setColour(bgColour); // Horizontal line - nvgDrawRoundedRect(nvg, circleBounds.getCentreX() - halfSize, circleBounds.getCentreY() - lineRad, crossSize, lineThickness, bgCol, bgCol, lineRad); + g.fillRoundedRectangle(circleBounds.getCentreX() - halfSize, circleBounds.getCentreY() - lineRad, crossSize, lineThickness, lineRad); // Vertical line - nvgDrawRoundedRect(nvg, circleBounds.getCentreX() - lineRad, circleBounds.getCentreY() - halfSize, lineThickness, crossSize, bgCol, bgCol, lineRad); + g.fillRoundedRectangle(circleBounds.getCentreX() - lineRad, circleBounds.getCentreY() - halfSize, lineThickness, crossSize, lineRad); - nvgFontFace(nvg, "Inter-Bold"); - nvgFontSize(nvg, 12); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_LEFT); - nvgFillColor(nvg, nvgColour(PlugDataColours::panelTextColour)); - nvgText(nvg, 92, 45, "New Patch", nullptr); + g.setColour(PlugDataColours::panelTextColour); + g.setFont(Fonts::getBoldFont().withPointHeight(12.0f)); + g.drawText("New Patch", Rectangle(92, 30, width - 104, 20), Justification::centredLeft, false); - nvgFontFace(nvg, "Inter-Regular"); - nvgText(nvg, 92, 63, "Create a new empty patch", nullptr); + g.setFont(Fonts::getDefaultFont().withPointHeight(12.0f)); + g.drawText("Create a new empty patch", Rectangle(92, 48, width - 104, 20), Justification::centredLeft, false); break; } case Open: { - nvgFontFace(nvg, "icon_font-Regular"); - nvgFillColor(nvg, bgCol); - nvgFontSize(nvg, 34); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgText(nvg, circleBounds.getCentreX(), circleBounds.getCentreY() - 4, Icons::Folder.toRawUTF8(), nullptr); - - nvgFontFace(nvg, "Inter-Bold"); - nvgFontSize(nvg, 12); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_LEFT); - nvgFillColor(nvg, nvgColour(PlugDataColours::panelTextColour)); - nvgText(nvg, 92, 45, "Open Patch...", nullptr); - - nvgFontFace(nvg, "Inter-Regular"); - nvgText(nvg, 92, 63, "Browse for a patch to open", nullptr); + g.setFont(Fonts::getIconFont().withPointHeight(34.0f)); + g.setColour(bgColour); + g.drawText(Icons::Folder, circleBounds.toNearestInt(), Justification::centred, false); + + g.setColour(PlugDataColours::panelTextColour); + g.setFont(Fonts::getBoldFont().withPointHeight(12.0f)); + g.drawText("Open Patch...", Rectangle(92, 30, width - 104, 20), Justification::centredLeft, false); + + g.setFont(Fonts::getDefaultFont().withPointHeight(12.0f)); + g.drawText("Browse for a patch to open", Rectangle(92, 48, width - 104, 20), Justification::centredLeft, false); break; } case Store: { - nvgFontFace(nvg, "icon_font-Regular"); - nvgFillColor(nvg, bgCol); - nvgFontSize(nvg, 30); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgText(nvg, circleBounds.getCentreX(), circleBounds.getCentreY() - 4, Icons::Sparkle.toRawUTF8(), nullptr); - - nvgFontFace(nvg, "Inter-Bold"); - nvgFontSize(nvg, 12); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_LEFT); - nvgFillColor(nvg, nvgColour(PlugDataColours::panelTextColour)); - nvgText(nvg, 92, 45, "Discover...", nullptr); - - nvgFontFace(nvg, "Inter-Regular"); - nvgText(nvg, 92, 63, "Browse online patch store", nullptr); + g.setFont(Fonts::getIconFont().withPointHeight(30.0f)); + g.setColour(bgColour); + g.drawText(Icons::Sparkle, circleBounds.toNearestInt(), Justification::centred, false); + + g.setColour(PlugDataColours::panelTextColour); + g.setFont(Fonts::getBoldFont().withPointHeight(12.0f)); + g.drawText("Discover...", Rectangle(92, 30, width - 104, 20), Justification::centredLeft, false); + + g.setFont(Fonts::getDefaultFont().withPointHeight(12.0f)); + g.drawText("Browse online patch store", Rectangle(92, 48, width - 104, 20), Justification::centredLeft, false); } default: break; @@ -231,7 +217,7 @@ class WelcomePanel final : public Component bool isHovered = false; String tileName, tileSubtitle; std::unique_ptr snapshot = nullptr; - NVGImage snapshotImage; + Image snapshotImage; File thumbnailFile; int lastWidth = -1; @@ -331,6 +317,7 @@ class WelcomePanel final : public Component auto const snapshotColour = PlugDataColours::objectSelectedOutlineColour.withAlpha(0.3f); snapshot->replaceColour(Colours::black, snapshotColour); } + snapshotImage = {}; resized(); } @@ -485,18 +472,21 @@ class WelcomePanel final : public Component { auto bounds = getLocalBounds().reduced(12); - auto* nvg = dynamic_cast(g.getInternalContext()).getContext(); - auto const scale = nvgCurrentPixelScale(nvg); - StackShadow::drawShadowForRect(g, bounds, 7, Corners::largeCornerRadius, 0.12f, 1); if (thumbnailFile != File()) { if (!snapshotImage.isValid() || lastWidth != bounds.getWidth() || lastHeight != bounds.getHeight()) { lastWidth = bounds.getWidth(); lastHeight = bounds.getHeight(); + auto const imageBounds = bounds.withTrimmedBottom(32); + + if (imageBounds.getWidth() > 0 && imageBounds.getHeight() > 0) { + snapshotImage = Image(Image::ARGB, imageBounds.getWidth(), imageBounds.getHeight(), true); + Graphics imageGraphics(snapshotImage); - snapshotImage = NVGImage(nvg, bounds.getWidth() * scale, (bounds.getHeight() - 32) * scale, [this, bounds, scale](Graphics& g) { - g.addTransform(AffineTransform::scale(scale)); + Path thumbnailClip; + thumbnailClip.addRoundedRectangle(0.0f, 0.0f, static_cast(imageBounds.getWidth()), static_cast(imageBounds.getHeight()), Corners::largeCornerRadius, Corners::largeCornerRadius, true, true, false, false); + imageGraphics.reduceClipRegion(thumbnailClip); auto thumbnailImageData = ImageFileFormat::loadFrom(thumbnailFile).convertedToFormat(Image::ARGB); if (thumbnailImageData.isValid()) { @@ -530,53 +520,50 @@ class WelcomePanel final : public Component offsetY = (componentHeight - drawHeight) / 2; } - g.drawImage(thumbnailImageData, offsetX, offsetY, drawWidth, drawHeight, 0, 0, imageWidth, imageHeight); + imageGraphics.drawImage(thumbnailImageData, offsetX, offsetY, drawWidth, drawHeight, 0, 0, imageWidth, imageHeight); } - }); - } - } else { - if (tileType != LibraryPatch && snapshot && !snapshotImage.isValid()) { - auto const scale = nvgCurrentPixelScale(nvg); - snapshotImage = NVGImage(nvg, bounds.getWidth() * scale, (bounds.getHeight() - 32) * scale, [this, scale](Graphics& g) { - g.addTransform(AffineTransform::scale(scale)); - snapshot->drawAt(g, 0, 0, 1.0f); - }); + } else { + snapshotImage = {}; + } } } - nvgSave(nvg); auto const lB = bounds.toFloat().expanded(0.5f); // Draw background even for images incase there is a transparent PNG - nvgDrawRoundedRect(nvg, lB.getX(), lB.getY(), lB.getWidth(), lB.getHeight(), nvgColour(PlugDataColours::panelForegroundColour), nvgColour(PlugDataColours::toolbarOutlineColour), Corners::largeCornerRadius); + g.setColour(PlugDataColours::panelForegroundColour); + g.fillRoundedRectangle(lB, Corners::largeCornerRadius); + g.setColour(PlugDataColours::toolbarOutlineColour); + g.drawRoundedRectangle(lB, Corners::largeCornerRadius, 1.0f); - nvgRoundedScissor(nvg, lB.getX(), lB.getY(), lB.getWidth(), lB.getHeight(), Corners::largeCornerRadius); - if (thumbnailFile != File() || tileType == Patch) { - nvgTranslate(nvg, 0.5f, 0.0f); // account for outline - snapshotImage.render(nvg, bounds.withTrimmedBottom(32)); + auto const imageBounds = bounds.withTrimmedBottom(32); + if (thumbnailFile != File()) { + if (snapshotImage.isValid()) + g.drawImageAt(snapshotImage, imageBounds.getX(), imageBounds.getY()); + } else if (tileType == Patch) { + if (snapshot) + snapshot->drawWithin(g, imageBounds.toFloat(), RectanglePlacement::centred, 1.0f); } else { auto const placeholderIconColour = PlugDataColours::objectSelectedOutlineColour.withAlpha(0.22f); + auto const placeholderIconBounds = Rectangle(bounds.getX(), (bounds.getHeight() - 30.0f) * 0.5f - 34.0f, bounds.getWidth(), 68.0f); // We draw the plugdata logo if library tiles don't have a thumbnail (patch snapshot is too busy) - nvgFillColor(nvg, nvgColour(placeholderIconColour)); - nvgFontFace(nvg, "icon_font-Regular"); - nvgFontSize(nvg, 68.0f); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgText(nvg, bounds.getCentreX(), (bounds.getHeight() - 30) * 0.5f, Icons::PlugdataIconStandard.toRawUTF8(), nullptr); + g.setFont(Fonts::getIconFont().withPointHeight(68.0f)); + g.setColour(placeholderIconColour); + g.drawText(Icons::PlugdataIconStandard, placeholderIconBounds, Justification::centred, false); } - nvgRestore(nvg); - // Draw border around - nvgDrawRoundedRect(nvg, lB.getX(), lB.getY(), lB.getWidth(), lB.getHeight(), nvgRGBA(0, 0, 0, 0), nvgColour(PlugDataColours::toolbarOutlineColour), Corners::largeCornerRadius); + g.setColour(PlugDataColours::toolbarOutlineColour); + g.drawRoundedRectangle(lB, Corners::largeCornerRadius, 1.0f); auto const hoverColour = PlugDataColours::toolbarHoverColour.interpolatedWith(PlugDataColours::toolbarBackgroundColour, 0.5f); - nvgBeginPath(nvg); - nvgRoundedRectVarying(nvg, bounds.getX(), bounds.getHeight() - 32, bounds.getWidth(), 44, 0.0f, 0.0f, Corners::largeCornerRadius, Corners::largeCornerRadius); - nvgFillColor(nvg, nvgColour(isHovered ? hoverColour : PlugDataColours::toolbarBackgroundColour)); - nvgFill(nvg); - nvgStrokeColor(nvg, nvgColour(PlugDataColours::toolbarOutlineColour)); - nvgStroke(nvg); + Path footerPath; + footerPath.addRoundedRectangle(bounds.getX(), bounds.getHeight() - 32, bounds.getWidth(), 44, Corners::largeCornerRadius, Corners::largeCornerRadius, false, false, true, true); + g.setColour(isHovered ? hoverColour : PlugDataColours::toolbarBackgroundColour); + g.fillPath(footerPath); + g.setColour(PlugDataColours::toolbarOutlineColour); + g.strokePath(footerPath, PathStrokeType(1.0f)); auto textWidth = bounds.getWidth() - 8; g.setColour(PlugDataColours::panelTextColour); @@ -588,14 +575,15 @@ class WelcomePanel final : public Component if (onFavourite) { auto const favouriteIconBounds = getHeartIconBounds(); - nvgFontFace(nvg, "icon_font-Regular"); if (isFavourited) { - nvgFillColor(nvg, nvgRGBA(250, 50, 40, 200)); - nvgText(nvg, favouriteIconBounds.getX(), favouriteIconBounds.getY() + 14, Icons::HeartFilled.toRawUTF8(), nullptr); + g.setFont(Fonts::getIconFont().withPointHeight(16.0f)); + g.setColour(Colour::fromRGBA(250, 50, 40, 200)); + g.drawText(Icons::HeartFilled, favouriteIconBounds, Justification::centred, false); } else if (isMouseOver()) { - nvgFillColor(nvg, nvgColour(PlugDataColours::panelTextColour)); - nvgText(nvg, favouriteIconBounds.getX(), favouriteIconBounds.getY() + 14, Icons::HeartStroked.toRawUTF8(), nullptr); + g.setFont(Fonts::getIconFont().withPointHeight(16.0f)); + g.setColour(PlugDataColours::panelTextColour); + g.drawText(Icons::HeartStroked, favouriteIconBounds, Justification::centred, false); } } } @@ -643,10 +631,9 @@ class WelcomePanel final : public Component void resized() override { - if (snapshot) { - auto const bounds = getLocalBounds().reduced(12).withTrimmedBottom(44); - snapshot->setTransformToFit(bounds.withSizeKeepingCentre(bounds.getWidth(), bounds.getHeight()).toFloat(), RectanglePlacement::centred); - } + snapshotImage = {}; + lastWidth = -1; + lastHeight = -1; } }; @@ -666,9 +653,7 @@ class WelcomePanel final : public Component viewport.setVisible(true); addChildComponent(viewport); - - setCachedComponentImage(new NVGSurface::InvalidationListener(editor->nvgSurface, this)); - + newPatchTile = std::make_unique(MainActionTile::New); openPatchTile = std::make_unique(MainActionTile::Open); storeTile = std::make_unique(MainActionTile::Store); @@ -1011,6 +996,11 @@ class WelcomePanel final : public Component } } + void paint(Graphics& g) override + { + g.fillAll(PlugDataColours::panelBackgroundColour); + } + void show() { triggerAsyncUpdate(); @@ -1022,20 +1012,6 @@ class WelcomePanel final : public Component setVisible(false); } - void render(NVGcontext* nvg) override - { - nvgDrawRoundedRect(nvg, -2, -Corners::windowCornerRadius, getWidth() + 4, getHeight() + Corners::windowCornerRadius, nvgColour(PlugDataColours::panelBackgroundColour), nvgColour(PlugDataColours::panelBackgroundColour), Corners::windowCornerRadius); - - Graphics g(*editor->getNanoLLGC()); - g.reduceClipRegion(editor->nvgSurface.getInvalidArea()); - paintEntireComponent(g, false); - - auto const gradient = nvgLinearGradient(nvg, 0, viewport.getY(), 0, viewport.getY() + 20, nvgColour(PlugDataColours::panelBackgroundColour), nvgRGBA(255, 255, 255, 0)); - - nvgFillPaint(nvg, gradient); - nvgFillRect(nvg, viewport.getX() + 8, viewport.getY(), viewport.getWidth() - 16, 20); - } - void lookAndFeelChanged() override { if (isVisible()) { diff --git a/Source/Connection.cpp b/Source/Connection.cpp index e3ed95fcb1..c5c7ef41d6 100644 --- a/Source/Connection.cpp +++ b/Source/Connection.cpp @@ -7,7 +7,7 @@ #include using namespace juce::gl; -#include +#include #include "Utility/Config.h" #include "Utility/NVGUtils.h" #include "Utility/SettingsFile.h" @@ -54,8 +54,6 @@ Connection::Connection(Canvas* parent, Iolet* s, Iolet* e, t_outconnect* oc) cableType = GemCable; } - setStrokeThickness(12.0f); // This will make sure the DrawablePath's bounds get expanded, which we use for hit detection and drawing reconnect handles - inIdx = inlet->getIndex(); outIdx = outlet->getIndex(); @@ -82,6 +80,7 @@ Connection::Connection(Canvas* parent, Iolet* s, Iolet* e, t_outconnect* oc) outobj->addComponentListener(this); inobj->addComponentListener(this); + setPaintingIsUnclipped(true); setInterceptsMouseClicks(true, true); addMouseListener(cnv, true); @@ -131,8 +130,6 @@ void Connection::lookAndFeelChanged() shadowColour = nvgColour(PlugDataColours::canvasBackgroundColour.contrasting(0.06f).withAlpha(0.24f)); outlineColour = nvgColour(PlugDataColours::objectOutlineColour); - textColour = nvgColour(PlugDataColours::objectSelectedOutlineColour.contrasting()); - if (connectionStyle != PlugDataLook::getConnectionStyle()) { connectionStyle = PlugDataLook::getConnectionStyle(); cachedPath.clear(); @@ -160,8 +157,8 @@ NVGcolor Connection::getConnectionColour() const void Connection::render(NVGcontext* nvg) { auto connectionColour = getConnectionColour(); - nvgSave(nvg); - nvgTranslate(nvg, getX(), getY()); + nanovg::nvgSave(nvg); + nanovg::nvgTranslate(nvg, getX(), getY()); bool isSignalCable = cableType == SignalCable && connectionStyle != PlugDataLook::ConnectionStyleVanilla; auto dashColor = shadowColour; @@ -181,23 +178,23 @@ void Connection::render(NVGcontext* nvg) pathFromOrigin.applyTransform(AffineTransform::translation(-getX(), -getY())); auto startPoint = pathFromOrigin.getPointAlongPath(0.0); - nvgBeginPath(nvg); - nvgFillColor(nvg, shadowColour); - nvgCircle(nvg, startPoint.x, startPoint.y, cableThickness * 0.5f); // cableThickness is diameter, while circle is radius - nvgFill(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgFillColor(nvg, shadowColour); + nanovg::nvgCircle(nvg, startPoint.x, startPoint.y, cableThickness * 0.5f); // cableThickness is diameter, while circle is radius + nanovg::nvgFill(nvg); - nvgBeginPath(nvg); - nvgFillColor(nvg, connectionColour); - nvgCircle(nvg, startPoint.x, startPoint.y, cableThickness * 0.25f); - nvgFill(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgFillColor(nvg, connectionColour); + nanovg::nvgCircle(nvg, startPoint.x, startPoint.y, cableThickness * 0.25f); + nanovg::nvgFill(nvg); return; } float dashSize = isSignalCable ? numSignalChannels <= 1 ? 2.5f : 1.5f : 0.0f; auto useGradientLook = PlugDataLook::getUseGradientConnectionLook() && !(isSelected() || isHovering); auto showActivity = cableType == DataCable && cnv->shouldShowConnectionActivity(); - nvgStrokePaint(nvg, nvgDoubleStroke(nvg, connectionColour, shadowColour, dashColor, dashSize, useGradientLook, showActivity, offset)); - nvgStrokeWidth(nvg, cableThickness); + nanovg::nvgStrokePaint(nvg, nanovg::nvgDoubleStroke(nvg, connectionColour, shadowColour, dashColor, dashSize, useGradientLook, showActivity, offset)); + nanovg::nvgStrokeWidth(nvg, cableThickness); bool cacheHit = cachedPath.stroke(); if (!cacheHit) { @@ -205,25 +202,25 @@ void Connection::render(NVGcontext* nvg) pathFromOrigin.applyTransform(AffineTransform::translation(-getX(), -getY())); setJUCEPath(nvg, pathFromOrigin); - nvgStroke(nvg); + nanovg::nvgStroke(nvg); cachedPath.save(nvg); } - nvgRestore(nvg); + nanovg::nvgRestore(nvg); if (isSelected() && isHovering) { auto expandedStartHandle = isInStartReconnectHandle ? startReconnectHandle.expanded(3.0f) : startReconnectHandle; auto expandedEndHandle = isInEndReconnectHandle ? endReconnectHandle.expanded(3.0f) : endReconnectHandle; - nvgFillColor(nvg, handleColour); + nanovg::nvgFillColor(nvg, handleColour); - nvgBeginPath(nvg); - nvgCircle(nvg, expandedStartHandle.getCentreX(), expandedStartHandle.getCentreY(), expandedStartHandle.getWidth() / 2); - nvgFill(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgCircle(nvg, expandedStartHandle.getCentreX(), expandedStartHandle.getCentreY(), expandedStartHandle.getWidth() / 2); + nanovg::nvgFill(nvg); - nvgBeginPath(nvg); - nvgCircle(nvg, expandedEndHandle.getCentreX(), expandedEndHandle.getCentreY(), expandedEndHandle.getWidth() / 2); - nvgFill(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgCircle(nvg, expandedEndHandle.getCentreX(), expandedEndHandle.getCentreY(), expandedEndHandle.getWidth() / 2); + nanovg::nvgFill(nvg); } // draw direction arrow if activated in overlay menu @@ -253,16 +250,16 @@ void Connection::render(NVGcontext* nvg) auto const point_c = cnv->getLocalPoint(this, arrowLine.getPointAlongLine(0.0f, arrowWidth * 0.5f)); // draw the arrow - nvgBeginPath(nvg); - nvgStrokeColor(nvg, outlineColour); - nvgFillColor(nvg, connectionColour); - nvgMoveTo(nvg, point_a.x, point_a.y); - nvgLineTo(nvg, point_b.x, point_b.y); - nvgLineTo(nvg, point_c.x, point_c.y); - nvgClosePath(nvg); - nvgStrokeWidth(nvg, 1.0f); - nvgFill(nvg); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgStrokeColor(nvg, outlineColour); + nanovg::nvgFillColor(nvg, connectionColour); + nanovg::nvgMoveTo(nvg, point_a.x, point_a.y); + nanovg::nvgLineTo(nvg, point_b.x, point_b.y); + nanovg::nvgLineTo(nvg, point_c.x, point_c.y); + nanovg::nvgClosePath(nvg); + nanovg::nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgFill(nvg); + nanovg::nvgStroke(nvg); }; if (cnv->shouldShowConnectionDirection()) { @@ -296,21 +293,19 @@ void Connection::renderConnectionOrder(NVGcontext* nvg) const connectionPath.applyTransform(AffineTransform::translation(-getX(), -getY())); auto const pos = cnv->getLocalPoint(this, connectionPath.getPointAlongPath(jmax(pathLength - 8.5f * 3, 9.5f))); // circle background - nvgBeginPath(nvg); - nvgStrokeColor(nvg, outlineColour); - nvgFillColor(nvg, getConnectionColour()); + nanovg::nvgBeginPath(nvg); + nanovg::nvgStrokeColor(nvg, outlineColour); + nanovg::nvgFillColor(nvg, getConnectionColour()); constexpr auto radius = 7.0f; constexpr auto diameter = radius * 2.0f; auto const circleTopLeft = pos - Point(radius, radius); - nvgRoundedRect(nvg, circleTopLeft.getX(), circleTopLeft.getY(), diameter, diameter, radius); - nvgStrokeWidth(nvg, 1.0f); - nvgFill(nvg); - nvgStroke(nvg); + nanovg::nvgRoundedRect(nvg, circleTopLeft.getX(), circleTopLeft.getY(), diameter, diameter, radius); + nanovg::nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgFill(nvg); + nanovg::nvgStroke(nvg); + // connection index number - nvgFillColor(nvg, textColour); - nvgFontSize(nvg, 9.0f); - nvgTextAlign(nvg, NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER); - nvgText(nvg, pos.getX(), pos.getY(), String(getMultiConnectNumber()).toUTF8(), nullptr); + Fonts::drawText(cnv->editor->getNanoLLGC(), String(getMultiConnectNumber()), Rectangle(radius, radius).withCentre(pos.toFloat()), Fonts::getCurrentFont().withHeight(9), PlugDataColours::objectSelectedOutlineColour.contrasting(), Justification::centred); } } @@ -726,12 +721,21 @@ int Connection::getClosestLineIdx(Point const& position, PathPlan const& return -1; } -void Connection::pathChanged() +void Connection::setPath(Path const& newPath) { - strokePath.clear(); - strokeType.createStrokedPath(strokePath, path, AffineTransform(), 1.0f); - setBoundsToEnclose(getDrawableBounds()); - repaint(); + path = newPath; + updateBounds(); +} + +void Connection::updateBounds() +{ + // Resize the component to enclose the path, expanded by the stroke thickness. + // The extra margin is used for hit detection and drawing the reconnect handles. + if (path.isEmpty()) { + setBounds({}); + return; + } + setBounds(path.getBounds().expanded(6.0f).getSmallestIntegerContainer()); } float Connection::getPathWidth() const diff --git a/Source/Connection.h b/Source/Connection.h index 92aea48da1..d8aeed4a2a 100644 --- a/Source/Connection.h +++ b/Source/Connection.h @@ -23,7 +23,7 @@ using PathPlan = SmallArray>; class Canvas; -class Connection final : public DrawablePath +class Connection final : public Component , public ComponentListener , public ChangeListener , public pd::MessageListener @@ -44,6 +44,10 @@ class Connection final : public DrawablePath static Path getNonSegmentedPath(Point start, Point end); + // Manual path/bounds management (previously provided by DrawablePath/DrawableComponent) + void setPath(Path const& newPath); + Path const& getPath() const { return path; } + bool isSegmented() const; void setSegmented(bool segmented); @@ -115,10 +119,14 @@ class Connection final : public DrawablePath void setSelected(bool shouldBeSelected); - void pathChanged() override; + // Recomputes the component bounds to enclose the current path, expanded by the + // stroke thickness. Used for hit detection and drawing the reconnect handles. + void updateBounds(); float getPathWidth() const; + Path path; + SmallArray> reconnecting; Rectangle startReconnectHandle, endReconnectHandle; @@ -127,11 +135,7 @@ class Connection final : public DrawablePath Value locked; Value presentationMode; - NVGcolor handleColour; - NVGcolor shadowColour; - NVGcolor outlineColour; - - NVGcolor textColour; + NVGcolor handleColour, shadowColour, outlineColour; RectangleList clipRegion; @@ -180,21 +184,22 @@ class Connection final : public DrawablePath JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Connection) }; -class ConnectionBeingCreated final : public DrawablePath +class ConnectionBeingCreated final : public Component , public NVGComponent { - SafePointer iolet; + Iolet::SafePointer iolet; Component* cnv; Point lastMousePos; + Path path; + public: ConnectionBeingCreated(Iolet* target, Component* canvas) : NVGComponent(this) , iolet(target) , cnv(canvas) { - setStrokeThickness(5.0f); - // Only listen for mouse-events on canvas and the original iolet + setPaintingIsUnclipped(true); setInterceptsMouseClicks(false, true); cnv->addMouseListener(this, true); iolet->addMouseListener(this, false); @@ -215,12 +220,23 @@ class ConnectionBeingCreated final : public DrawablePath } } - void pathChanged() override + // Manual path/bounds management (previously provided by DrawablePath/DrawableComponent) + void setPath(Path const& newPath) { - strokePath.clear(); - strokePath = path; - setBoundsToEnclose(getDrawableBounds().expanded(3)); - repaint(); + path = newPath; + updateBounds(); + } + + Path const& getPath() const { return path; } + + // Resizes the component to enclose the path, expanded by the stroke thickness + void updateBounds() + { + if (path.isEmpty()) { + setBounds({}); + return; + } + setBounds(path.getBounds().expanded(2.5f).getSmallestIntegerContainer()); } void mouseDrag(MouseEvent const& e) override @@ -281,7 +297,7 @@ class ConnectionBeingCreated final : public DrawablePath break; } - nvgStrokeWidth(nvg, cableThickness); + nanovg::nvgStrokeWidth(nvg, cableThickness); if (iolet && iolet->isSignal() && connectionStyle != PlugDataLook::ConnectionStyleVanilla) { auto const lineColour = PlugDataColours::signalColour.brighter(0.6f); @@ -290,18 +306,18 @@ class ConnectionBeingCreated final : public DrawablePath dashColor.r *= 0.4f; dashColor.g *= 0.4f; dashColor.b *= 0.4f; - nvgStrokePaint(nvg, nvgDoubleStroke(nvg, nvgColour(lineColour), nvgColour(shadowColour), dashColor, 2.5f, false, false, 0.0f)); - nvgStroke(nvg); + nanovg::nvgStrokePaint(nvg, nanovg::nvgDoubleStroke(nvg, nvgColour(lineColour), nvgColour(shadowColour), dashColor, 2.5f, false, false, 0.0f)); + nanovg::nvgStroke(nvg); } else { auto const lineColour = PlugDataColours::dataColour.brighter(0.6f); - nvgStrokePaint(nvg, nvgDoubleStroke(nvg, nvgColour(lineColour), nvgColour(shadowColour), nvgColour(Colours::transparentBlack), 0.0f, false, false, 0.0f)); - nvgStroke(nvg); + nanovg::nvgStrokePaint(nvg, nanovg::nvgDoubleStroke(nvg, nvgColour(lineColour), nvgColour(shadowColour), nvgColour(Colours::transparentBlack), 0.0f, false, false, 0.0f)); + nanovg::nvgStroke(nvg); } - nvgBeginPath(nvg); - nvgFillColor(nvg, nvgRGBAf(0.6f, 0.6f, 0.6f, 0.7f)); - nvgCircle(nvg, lastMousePos.x, lastMousePos.y, 3.5f); - nvgFill(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgFillColor(nvg, nanovg::nvgRGBAf(0.6f, 0.6f, 0.6f, 0.7f)); + nanovg::nvgCircle(nvg, lastMousePos.x, lastMousePos.y, 3.5f); + nanovg::nvgFill(nvg); } void toNextIolet() diff --git a/Source/Constants.h b/Source/Constants.h index 3ba28cb027..5f9cd78c6c 100644 --- a/Source/Constants.h +++ b/Source/Constants.h @@ -43,6 +43,8 @@ struct Icons { static inline String const Message = "D"; static inline String const Parameters = "E"; static inline String const Presentation = "F"; + // TODO: placeholder (reuses the Lock glyph) until a dedicated play glyph is added to the icon font + static inline String const Play = "m"; static inline String const Externals = "G"; static inline String const Refresh = "H"; static inline String const Up = "I"; @@ -319,10 +321,6 @@ enum PlugDataColour { sidebarTextColourId, sidebarActiveBackgroundColourId, - levelMeterActiveColourId, - levelMeterBackgroundColourId, - levelMeterThumbColourId, - panelBackgroundColourId, panelForegroundColourId, panelTextColourId, diff --git a/Source/Dialogs/AdvancedSettingsPanel.h b/Source/Dialogs/AdvancedSettingsPanel.h index 8c817c4932..eb81095ed7 100644 --- a/Source/Dialogs/AdvancedSettingsPanel.h +++ b/Source/Dialogs/AdvancedSettingsPanel.h @@ -41,10 +41,6 @@ class AdvancedSettingsPanel final : public SettingsDialogPanel } #endif - floatingPanelsValue.referTo(settingsFile->getPropertyAsValue("floating_panels")); - floatingPanelsValue.addListener(this); - interfaceProperties.add(new PropertiesPanel::BoolComponent("Floating panels", floatingPanelsValue, { "No", "Yes" })); - commandClickSwitchesModeValue.referTo(settingsFile->getPropertyAsValue("cmd_click_switches_mode")); commandClickSwitchesModeValue.addListener(this); #if JUCE_MAC @@ -163,6 +159,9 @@ class AdvancedSettingsPanel final : public SettingsDialogPanel { if (v.refersToSameSourceAs(nativeTitlebar)) { // Make sure titlebar buttons are greyed out because a dialog is still showing + if (auto* pluginEditor = dynamic_cast(editor)) + pluginEditor->setStandaloneWindowControlsEnabled(false); + if (auto const* window = dynamic_cast(getTopLevelComponent())) { if (auto* closeButton = window->getCloseButton()) closeButton->setEnabled(false); @@ -175,9 +174,6 @@ class AdvancedSettingsPanel final : public SettingsDialogPanel if (v.refersToSameSourceAs(showPalettesValue)) { editor->resized(); } - if (v.refersToSameSourceAs(floatingPanelsValue)) { - editor->resized(); - } if (v.refersToSameSourceAs(scaleValue)) { SettingsFile::getInstance()->setGlobalScale(getValue(scaleValue)); } @@ -199,7 +195,6 @@ class AdvancedSettingsPanel final : public SettingsDialogPanel Value openPatchesInWindow; Value showPalettesValue; - Value floatingPanelsValue; Value autoPatchingValue; Value showAllAudioDeviceValues; Value nativeDialogValue; diff --git a/Source/Dialogs/AudioSettingsPanel.h b/Source/Dialogs/AudioSettingsPanel.h index 98314de10e..2cb8f42090 100644 --- a/Source/Dialogs/AudioSettingsPanel.h +++ b/Source/Dialogs/AudioSettingsPanel.h @@ -41,7 +41,7 @@ class DeviceManagerLevelMeter final : public Component g.setColour(findColour(TextButton::buttonColourId)); g.fillRoundedRectangle(getLocalBounds().toFloat(), getHeight() / 2.0f); - g.setColour(PlugDataColours::levelMeterActiveColour); + g.setColour(PlugDataColours::dataColour); g.fillRoundedRectangle(getLocalBounds().toFloat().withWidth(levelWidth), getHeight() / 2.0f); } diff --git a/Source/Dialogs/Deken.h b/Source/Dialogs/Deken.h index c1352d75b7..5391067577 100644 --- a/Source/Dialogs/Deken.h +++ b/Source/Dialogs/Deken.h @@ -96,7 +96,13 @@ class PackageManager final : public Thread .withConnectionTimeoutMs(10000) .withStatusCode(&statusCode)); - if (instream != nullptr && statusCode == 200) { + bool downloadOk = instream != nullptr && statusCode == 200; +#if ENABLE_TESTING + // Local file:// archives used by the test suite don't report an + // HTTP status, so accept any readable stream while testing + downloadOk = instream != nullptr; +#endif + if (downloadOk) { startThread(); } else { finish(Result::fail("Failed to start download")); @@ -228,24 +234,37 @@ class PackageManager final : public Thread PackageList getAvailablePackages() { + MemoryBlock block; - // plugdata's deken servers, hosted on GitHub - // This will pre-parse the deken repo information to a faster and smaller format - // This saves a lot of work that plugdata would have to do on startup! - - auto const triplet = os + "-" + machine + "-" + floatsize; - auto const repoForArchitecture = "https://raw.githubusercontent.com/plugdata-team/plugdata-deken/main/bin/" + triplet + ".bin"; - - webstream = std::make_unique(URL(repoForArchitecture), false); - webstream->connect(nullptr); - - if (webstream->isError()) { +#if ENABLE_TESTING + // The test suite injects a serialised package tree (or forces a failure) + // here, so no network request is made during testing + if (mockShouldFail) { sendActionMessage("Failed to connect to server"); return { }; } + if (mockPackageData.getSize() > 0) { + block = mockPackageData; + } else +#endif + { + // plugdata's deken servers, hosted on GitHub + // This will pre-parse the deken repo information to a faster and smaller format + // This saves a lot of work that plugdata would have to do on startup! - MemoryBlock block; - webstream->readIntoMemoryBlock(block); + auto const triplet = os + "-" + machine + "-" + floatsize; + auto const repoForArchitecture = "https://raw.githubusercontent.com/plugdata-team/plugdata-deken/main/bin/" + triplet + ".bin"; + + webstream = std::make_unique(URL(repoForArchitecture), false); + webstream->connect(nullptr); + + if (webstream->isError()) { + sendActionMessage("Failed to connect to server"); + return { }; + } + + webstream->readIntoMemoryBlock(block); + } // Parse tree that was downloaded auto const tree = ValueTree::readFromData(block.getData(), block.getSize()); @@ -345,7 +364,15 @@ class PackageManager final : public Thread PackageList allPackages; +#if ENABLE_TESTING + // The test suite installs into a temp dir and injects mock catalog data, + // so neither the network nor the user's real Externals folder is touched + static inline File filesystem = ProjectInfo::appDataDir.getChildFile("Externals"); + static inline MemoryBlock mockPackageData; + static inline bool mockShouldFail = false; +#else static inline File const filesystem = ProjectInfo::appDataDir.getChildFile("Externals"); +#endif // Package info file File pkgInfo = filesystem.getChildFile(".pkg_info"); @@ -409,7 +436,9 @@ class PackageManager final : public Thread JUCE_DECLARE_SINGLETON(PackageManager, false) }; +#ifndef PLUGDATA_TEST_TRANSLATION_UNIT // implemented in Dialogs.cpp's TU JUCE_IMPLEMENT_SINGLETON(PackageManager) +#endif class Deken final : public Component , public ListBoxModel diff --git a/Source/Dialogs/Dialogs.cpp b/Source/Dialogs/Dialogs.cpp index ae38f01441..f9f745c13f 100644 --- a/Source/Dialogs/Dialogs.cpp +++ b/Source/Dialogs/Dialogs.cpp @@ -80,7 +80,7 @@ Dialog::Dialog(std::unique_ptr* ownerPtr, Component* editor, int const c } if (auto* pluginEditor = dynamic_cast(editor)) { - pluginEditor->nvgSurface.setRenderThroughImage(true); + pluginEditor->setStandaloneWindowControlsEnabled(false); } } @@ -766,8 +766,14 @@ void Dialogs::showCanvasRightClickMenu(Canvas* cnv, Component* originalComponent popupMenu.addSeparator(); popupMenu.addItem(Properties, "Properties", (originalComponent == cnv || (object && params.getParameters().not_empty())) && !locked); - // showObjectReferenceDialog - auto callback = [cnv, editor, object, originalComponent, selectedObjects](int const result) mutable { + + auto callback = [cnvPtr = Component::SafePointer(cnv), editor, objectPtr = Component::SafePointer(object), originalPtr = Component::SafePointer(originalComponent), selectedObjects](int const result) mutable { + auto* cnv = cnvPtr.getComponent(); + if (!cnv) + return; + auto* object = objectPtr.getComponent(); + auto* originalComponent = originalPtr.getComponent(); + cnv->grabKeyboardFocus(); editor->showCalloutArea(false); @@ -937,6 +943,9 @@ void Dialogs::dismissFileDialog() void Dialogs::showOpenDialog(std::function const& callback, bool const canSelectFiles, bool const canSelectDirectories, String const& extension, String const& lastFileId, Component* parentComponent) { +#if ENABLE_TESTING + return; // Don't open file dialogs during testing +#endif bool nativeDialog = SettingsFile::getInstance()->wantsNativeDialog(); auto initialFile = lastFileId.isNotEmpty() ? SettingsFile::getInstance()->getLastBrowserPathForId(lastFileId) : ProjectInfo::appDataDir; if (!initialFile.exists()) @@ -980,6 +989,9 @@ void Dialogs::showOpenDialog(std::function const& callback, bool cons void Dialogs::showSaveDialog(std::function const& callback, String const& extension, String const& lastFileId, Component* parentComponent, bool const directoryMode, String const& defaultFileName) { +#if ENABLE_TESTING + return; // Don't open file dialogs during testing +#endif bool nativeDialog = SettingsFile::getInstance()->wantsNativeDialog(); auto initialFile = lastFileId.isNotEmpty() ? SettingsFile::getInstance()->getLastBrowserPathForId(lastFileId) : ProjectInfo::appDataDir; if (!initialFile.exists()) diff --git a/Source/Dialogs/Dialogs.h b/Source/Dialogs/Dialogs.h index 637a82a8b4..4271d59a8d 100644 --- a/Source/Dialogs/Dialogs.h +++ b/Source/Dialogs/Dialogs.h @@ -21,7 +21,7 @@ class Dialog final : public Component { ~Dialog() override { if (auto* editor = dynamic_cast(parentComponent)) { - editor->nvgSurface.setRenderThroughImage(false); + editor->setStandaloneWindowControlsEnabled(true); } if (auto const* window = dynamic_cast(getTopLevelComponent())) { diff --git a/Source/Dialogs/HelpDialog.h b/Source/Dialogs/HelpDialog.h index 13827e38ae..c11c2062ea 100644 --- a/Source/Dialogs/HelpDialog.h +++ b/Source/Dialogs/HelpDialog.h @@ -114,7 +114,7 @@ class HelpDialog final : public TopLevelWindow constrainer.setFixedAspectRatio(0.0f); resizer = std::make_unique>(this, &constrainer); - // resizer->setAllowHostManagedResize(false); + resizer->setAllowHostManagedResize(false); resizer->setAlwaysOnTop(true); addAndMakeVisible(resizer.get()); diff --git a/Source/Dialogs/ObjectBrowserDialog.h b/Source/Dialogs/ObjectBrowserDialog.h index 773b72d142..bcc7b11057 100644 --- a/Source/Dialogs/ObjectBrowserDialog.h +++ b/Source/Dialogs/ObjectBrowserDialog.h @@ -265,7 +265,6 @@ class ObjectViewerDragArea final : public Component { : editor(editor) , dismissMenu(dismissMenu) { - setBufferedToImage(true); } ~ObjectViewerDragArea() override { } @@ -360,8 +359,6 @@ class ObjectViewer final : public Component { button->setColour(TextButton::buttonColourId, PlugDataColours::panelBackgroundColour); button->setColour(TextButton::buttonOnColourId, PlugDataColours::panelActiveBackgroundColour); } - - setBufferedToImage(true); } void resized() override @@ -501,15 +498,15 @@ class ObjectViewer final : public Component { Rectangle bb(x, y, static_cast(ioletSize), static_cast(ioletSize)); Colour fill = ports[i] ? PlugDataColours::signalColour : PlugDataColours::dataColour; - g.setColour(fill); + g.setColour(PlugDataColours::objectOutlineColour); if (squareIolets) { - g.fillRect(bb); - g.setColour(PlugDataColours::objectOutlineColour); g.drawRect(bb, 1.0f); + g.setColour(fill); + g.fillRect(bb.reduced(0.5f)); } else { - g.fillEllipse(bb); - g.setColour(PlugDataColours::objectOutlineColour); g.drawEllipse(bb, 1.0f); + g.setColour(fill); + g.fillEllipse(bb.reduced(0.5f)); } } }; diff --git a/Source/Dialogs/ObjectReferenceDialog.h b/Source/Dialogs/ObjectReferenceDialog.h index 87194da91f..739b5cf961 100644 --- a/Source/Dialogs/ObjectReferenceDialog.h +++ b/Source/Dialogs/ObjectReferenceDialog.h @@ -15,7 +15,6 @@ class ObjectReferenceDialog final : public Component { ObjectReferenceDialog(PluginEditor const* editor, bool const showBackButton) : library(*editor->pd->objectLibrary) { - setBufferedToImage(true); if (showBackButton) { addAndMakeVisible(backButton); @@ -325,15 +324,15 @@ class ObjectReferenceDialog final : public Component { Rectangle bb(x, y, static_cast(ioletSize), static_cast(ioletSize)); Colour fill = ports[i] ? PlugDataColours::signalColour : PlugDataColours::dataColour; - g.setColour(fill); + g.setColour(PlugDataColours::objectOutlineColour); if (squareIolets) { - g.fillRect(bb); - g.setColour(PlugDataColours::objectOutlineColour); g.drawRect(bb, 1.0f); + g.setColour(fill); + g.fillRect(bb.reduced(0.5f)); } else { - g.fillEllipse(bb); - g.setColour(PlugDataColours::objectOutlineColour); g.drawEllipse(bb, 1.0f); + g.setColour(fill); + g.fillEllipse(bb.reduced(0.5f)); } } }; diff --git a/Source/Dialogs/OnboardingDialog.h b/Source/Dialogs/OnboardingDialog.h index a9c10376e1..936ff6fb93 100644 --- a/Source/Dialogs/OnboardingDialog.h +++ b/Source/Dialogs/OnboardingDialog.h @@ -93,45 +93,6 @@ static void drawDockedWindowMockup(Graphics& g, Rectangle bounds, titleBar.getRight(), titleBar.getBottom(), 1.0f); } -static void drawFloatingWindowMockup(Graphics& g, Rectangle bounds, - OnboardingMockupColours const& c) -{ - auto window = bounds.reduced(6.0f); - - g.setColour(c.canvasBg); - g.fillRoundedRectangle(window, Corners::defaultCornerRadius); - - auto titleBar = window.withHeight(14.0f); - Path titleClip; - titleClip.addRoundedRectangle(titleBar.getX(), titleBar.getY(), - titleBar.getWidth(), titleBar.getHeight(), - Corners::defaultCornerRadius, Corners::defaultCornerRadius, true, true, false, false); - g.setColour(c.toolbarBg); - g.fillPath(titleClip); - for (int i = 0; i < 3; ++i) { - g.setColour(c.canvasText.withAlpha(0.45f)); - g.fillEllipse(titleBar.getX() + 6 + i * 7, titleBar.getCentreY() - 2.5f, 5, 5); - } - - auto inner = window.withTrimmedTop(14.0f); - auto leftMini = inner.removeFromLeft(20).withSizeKeepingCentre(10, 32); - auto rightMini = inner.removeFromRight(20).withSizeKeepingCentre(10, 32); - auto bottomMini = inner.removeFromBottom(20).withSizeKeepingCentre(40, 10); - - for (auto r : { leftMini, rightMini, bottomMini }) { - g.setColour(c.sidebarBg); - g.fillRoundedRectangle(r, 4.0f); - g.setColour(c.outline.withAlpha(0.85f)); - g.drawRoundedRectangle(r, 4.0f, 1.0f); - } - - g.setColour(c.outline); - g.drawRoundedRectangle(window, Corners::defaultCornerRadius, 1.0f); - g.drawLine(titleBar.getX(), titleBar.getBottom(), - titleBar.getRight(), titleBar.getBottom(), 1.0f); -} - - class OnboardingPageIndicator final : public Component { public: int numPages = 1; @@ -547,72 +508,6 @@ class OnboardingThemePage final : public OnboardingPage { String selectedAltTheme; }; -class OnboardingPanelChoiceCard final : public OnboardingCard { -public: - bool floating; - String label; - - OnboardingPanelChoiceCard(bool isFloating, String lbl) - : floating(isFloating), label(std::move(lbl)) {} - - void paintCardContent(Graphics& g, Rectangle bounds) override - { - auto labelArea = bounds.removeFromBottom(28.0f); - auto cols = OnboardingMockupColours::fromCurrent(*this); - if (floating) drawFloatingWindowMockup(g, bounds, cols); - else drawDockedWindowMockup(g, bounds, cols, /*drawObject=*/false); - - g.setColour(findColour(PlugDataColour::panelTextColourId)); - g.setFont(Fonts::getSemiBoldFont().withHeight(15.0f)); - g.drawText(label, labelArea.toNearestInt(), Justification::centred); - } -}; - -class OnboardingPanelsPage final : public OnboardingPage { -public: - OnboardingPanelsPage() - : OnboardingPage("Panel layout", - "Choose your preferred UI layout.") - { - addAndMakeVisible(docked); - addAndMakeVisible(floating); - docked.onClick = [this] { setFloating(false); }; - floating.onClick = [this] { setFloating(true); }; - setFloating(SettingsFile::getInstance()->getProperty("floating_panels") != 0); - } - - void resized() override - { - auto area = getContentArea().reduced(10); - int const cardW = std::min(260, (area.getWidth() - 24) / 2); - int const cardH = std::min(area.getHeight(), 250); - int const totalW = cardW * 2 + 24; - int const x = area.getCentreX() - totalW / 2; - int const y = area.getCentreY() - cardH / 2; - docked.setBounds(x, y, cardW, cardH); - floating.setBounds(x + cardW + 24, y, cardW, cardH); - } - - void apply() override - { - SettingsFile::getInstance()->setProperty("floating_panels", var(useFloating)); - } - -private: - void setFloating(bool f) - { - useFloating = f; - docked.selected = !f; - floating.selected = f; - docked.repaint(); - floating.repaint(); - } - - OnboardingPanelChoiceCard docked { false, "Docked" }; - OnboardingPanelChoiceCard floating { true, "Floating" }; - bool useFloating = false; -}; - class OnboardingKeymapCard final : public OnboardingCard { public: String name; @@ -1116,7 +1011,6 @@ class OnboardingDialog final : public Component { pages.add(new OnboardingWelcomePage()); pages.add(new OnboardingUseCasePage()); pages.add(new OnboardingThemePage()); - pages.add(new OnboardingPanelsPage()); pages.add(new OnboardingKeymapPage(editor)); pages.add(new OnboardingDisplayPage()); pages.add(new OnboardingDocsPage()); diff --git a/Source/Dialogs/PatchStore.h b/Source/Dialogs/PatchStore.h index be39664cc1..6a406a155a 100644 --- a/Source/Dialogs/PatchStore.h +++ b/Source/Dialogs/PatchStore.h @@ -47,10 +47,12 @@ class DownloadPool final : public DeletedAtShutdown { cancelImageDownloads(); imagePool.addJob([this] { SmallArray patches; - int statusCode = 0; - auto const webstream = URL("https://plugdata.org/store.json").createInputStream(URL::InputStreamOptions(URL::ParameterHandling::inAddress).withConnectionTimeoutMs(10000).withStatusCode(&statusCode)); - if (!webstream || statusCode >= 400) { + String jsonString; +#if ENABLE_TESTING + // The test suite injects mock catalog JSON (or forces a failure) + // here, so no network request is made during testing + if (mockStoreShouldFail) { MessageManager::callAsync([this] { for (auto& listener : listeners) { listener->databaseDownloadFailed(); @@ -58,18 +60,36 @@ class DownloadPool final : public DeletedAtShutdown { }); return; } + if (mockStoreJson.isNotEmpty()) { + jsonString = mockStoreJson; + } else +#endif + { + int statusCode = 0; + auto const webstream = URL("https://plugdata.org/store.json").createInputStream(URL::InputStreamOptions(URL::ParameterHandling::inAddress).withConnectionTimeoutMs(10000).withStatusCode(&statusCode)); - MemoryBlock jsonData; - MemoryOutputStream mo(jsonData, false); + if (!webstream || statusCode >= 400) { + MessageManager::callAsync([this] { + for (auto& listener : listeners) { + listener->databaseDownloadFailed(); + } + }); + return; + } - mo.preallocate(32000); // fit store.json file with some extra space - while (true) { - auto const written = mo.writeFromInputStream(*webstream, 1 << 14); - if (written == 0) - break; + MemoryBlock jsonData; + MemoryOutputStream mo(jsonData, false); + + mo.preallocate(32000); // fit store.json file with some extra space + while (true) { + auto const written = mo.writeFromInputStream(*webstream, 1 << 14); + if (written == 0) + break; + } + jsonString = jsonData.toString(); // Converting to string is important on Windows to get correct character encoding } - auto const parsedData = JSON::parse(jsonData.toString()); // Converting to string is important on Windows to get correct character encoding + auto const parsedData = JSON::parse(jsonString); auto patchData = parsedData["Patches"]; if (patchData.isArray()) { for (int i = 0; i < patchData.size(); ++i) { @@ -262,13 +282,22 @@ class DownloadPool final : public DeletedAtShutdown { ThreadPool imagePool = ThreadPool(3); ThreadPool patchPool = ThreadPool(2); - std::atomic cancelledImageDownload = false; + AtomicValue cancelledImageDownload = false; public: +#if ENABLE_TESTING + // The test suite injects mock catalog JSON (or forces a failure) so the + // store database can be exercised without any network access + static inline String mockStoreJson; + static inline bool mockStoreShouldFail = false; +#endif + JUCE_DECLARE_SINGLETON(DownloadPool, false); }; +#ifndef PLUGDATA_TEST_TRANSLATION_UNIT // implemented in Dialogs.cpp's TU JUCE_IMPLEMENT_SINGLETON(DownloadPool); +#endif class OnlineImage final : public Component , public DownloadPool::DownloadListener { @@ -426,8 +455,8 @@ class OnlineImage final : public Component URL imageURL; Image image; Spinner spinner; - std::atomic imageHash; - std::atomic width, height; + AtomicValue imageHash; + AtomicValue width, height; static inline float scale = 0.0f; diff --git a/Source/Dialogs/TextEditorDialog.h b/Source/Dialogs/TextEditorDialog.h index 0f686364b5..e01ff48e1e 100644 --- a/Source/Dialogs/TextEditorDialog.h +++ b/Source/Dialogs/TextEditorDialog.h @@ -1370,26 +1370,26 @@ class PlugDataTextEditor final : public Component { // IMPLEMENTATIONS -Caret::Caret(TextDocument const& document) +inline Caret::Caret(TextDocument const& document) : document(document) { setInterceptsMouseClicks(false, false); startTimerHz(20); } -void Caret::setViewTransform(AffineTransform const& transformToUse) +inline void Caret::setViewTransform(AffineTransform const& transformToUse) { transform = transformToUse; repaint(); } -void Caret::updateSelections() +inline void Caret::updateSelections() { phase = 0.f; repaint(); } -void Caret::paint(Graphics& g) +inline void Caret::paint(Graphics& g) { g.setColour(getParentComponent()->findColour(CaretComponent::caretColourId).withAlpha(squareWave(phase))); @@ -1397,14 +1397,14 @@ void Caret::paint(Graphics& g) g.fillRect(r); } -float Caret::squareWave(float const wt) +inline float Caret::squareWave(float const wt) { constexpr float delta = 0.222f; constexpr float A = 1.0; return 0.5f + A / 3.14159f * std::atan(std::cos(wt) / delta); } -void Caret::timerCallback() +inline void Caret::timerCallback() { phase += 3.2e-1; @@ -1412,7 +1412,7 @@ void Caret::timerCallback() repaint(r.getSmallestIntegerContainer()); } -SmallArray> Caret::getCaretRectangles() const +inline SmallArray> Caret::getCaretRectangles() const { SmallArray> rectangles; @@ -1429,18 +1429,18 @@ SmallArray> Caret::getCaretRectangles() const return rectangles; } -GutterComponent::GutterComponent(TextDocument const& document) +inline GutterComponent::GutterComponent(TextDocument const& document) : document(document) { setInterceptsMouseClicks(false, false); } -void GutterComponent::updateSelections() +inline void GutterComponent::updateSelections() { repaint(); } -void GutterComponent::paint(Graphics& g) +inline void GutterComponent::paint(Graphics& g) { auto const ln = PlugDataColours::sidebarBackgroundColour; auto const scaleFactor = std::sqrt(std::abs(transform.getDeterminant())); @@ -1480,19 +1480,19 @@ void GutterComponent::paint(Graphics& g) } } -HighlightComponent::HighlightComponent(TextDocument const& document) +inline HighlightComponent::HighlightComponent(TextDocument const& document) : document(document) { setInterceptsMouseClicks(false, false); } -void HighlightComponent::setViewTransform(AffineTransform const& transformToUse, SmallArray const& selections) +inline void HighlightComponent::setViewTransform(AffineTransform const& transformToUse, SmallArray const& selections) { transform = transformToUse; updateSelections(selections, lastMainSelection); } -void HighlightComponent::updateSelections(SmallArray const& selections, int mainSelection) +inline void HighlightComponent::updateSelections(SmallArray const& selections, int mainSelection) { lastMainSelection = mainSelection; outlinePath.clear(); @@ -1512,7 +1512,7 @@ void HighlightComponent::updateSelections(SmallArray const& selection repaint(outlinePath.getBounds().getSmallestIntegerContainer()); } -void HighlightComponent::paint(Graphics& g) +inline void HighlightComponent::paint(Graphics& g) { g.addTransform(transform); @@ -1526,7 +1526,7 @@ void HighlightComponent::paint(Graphics& g) g.strokePath(outlinePath, PathStrokeType(1.f)); } -Path HighlightComponent::getOutlinePath(SmallArray> const& rectangles) +inline Path HighlightComponent::getOutlinePath(SmallArray> const& rectangles) { auto p = Path(); auto rect = rectangles.begin(); @@ -1551,7 +1551,7 @@ Path HighlightComponent::getOutlinePath(SmallArray> const& rect return p.createPathWithRoundedCorners(4.f); } -Selection::Selection(String const& content) +inline Selection::Selection(String const& content) { int rowSpan = 0; int n = 0, lastLineStart = 0; @@ -1570,12 +1570,12 @@ Selection::Selection(String const& content) tail = { rowSpan, content.length() - lastLineStart }; } -bool Selection::isOriented() const +inline bool Selection::isOriented() const { return !(head.x > tail.x || (head.x == tail.x && head.y > tail.y)); } -Selection Selection::oriented() const +inline Selection Selection::oriented() const { if (!isOriented()) return swapped(); @@ -1583,14 +1583,14 @@ Selection Selection::oriented() const return *this; } -Selection Selection::swapped() const +inline Selection Selection::swapped() const { Selection s = *this; std::swap(s.head, s.tail); return s; } -Selection Selection::horizontallyMaximized(TextDocument const& document) const +inline Selection Selection::horizontallyMaximized(TextDocument const& document) const { Selection s = *this; @@ -1604,7 +1604,7 @@ Selection Selection::horizontallyMaximized(TextDocument const& document) const return s; } -Selection Selection::measuring(String const& content) const +inline Selection Selection::measuring(String const& content) const { Selection s(content); @@ -1614,7 +1614,7 @@ Selection Selection::measuring(String const& content) const return Selection(content).startingFrom(tail).swapped(); } -Selection Selection::startingFrom(Point const index) const +inline Selection Selection::startingFrom(Point const index) const { Selection s = *this; @@ -1631,19 +1631,19 @@ Selection Selection::startingFrom(Point const index) const return s; } -void Selection::pullBy(Selection disappearingSelection) +inline void Selection::pullBy(Selection disappearingSelection) { disappearingSelection.pull(head); disappearingSelection.pull(tail); } -void Selection::pushBy(Selection appearingSelection) +inline void Selection::pushBy(Selection appearingSelection) { appearingSelection.push(head); appearingSelection.push(tail); } -void Selection::pull(Point& index) const +inline void Selection::pull(Point& index) const { auto const S = oriented(); @@ -1670,7 +1670,7 @@ void Selection::pull(Point& index) const } } -void Selection::push(Point& index) const +inline void Selection::push(Point& index) const { auto const S = oriented(); @@ -1696,7 +1696,7 @@ void Selection::push(Point& index) const } } -String const& GlyphArrangementArray::operator[](int const index) const +inline String const& GlyphArrangementArray::operator[](int const index) const { if (isPositiveAndBelow(index, lines.size())) { return lines[index].string; @@ -1706,7 +1706,7 @@ String const& GlyphArrangementArray::operator[](int const index) const return empty; } -int GlyphArrangementArray::getToken(int const row, int const col, int const defaultIfOutOfBounds) const +inline int GlyphArrangementArray::getToken(int const row, int const col, int const defaultIfOutOfBounds) const { if (!isPositiveAndBelow(row, lines.size())) { return defaultIfOutOfBounds; @@ -1714,12 +1714,12 @@ int GlyphArrangementArray::getToken(int const row, int const col, int const defa return lines[row].tokens[col]; } -bool GlyphArrangementArray::isNewLine(int index) const +inline bool GlyphArrangementArray::isNewLine(int index) const { return lines[index].isNewLine; } -void GlyphArrangementArray::clearTokens(int const index) +inline void GlyphArrangementArray::clearTokens(int const index) { if (!isPositiveAndBelow(index, lines.size())) return; @@ -1733,7 +1733,7 @@ void GlyphArrangementArray::clearTokens(int const index) } } -void GlyphArrangementArray::applyTokens(int const index, Selection zone) +inline void GlyphArrangementArray::applyTokens(int const index, Selection zone) { if (!isPositiveAndBelow(index, lines.size())) return; @@ -1748,7 +1748,7 @@ void GlyphArrangementArray::applyTokens(int const index, Selection zone) } } -GlyphArrangement GlyphArrangementArray::getGlyphs(int const index, +inline GlyphArrangement GlyphArrangementArray::getGlyphs(int const index, float const baseline, int const token, bool const withTrailingSpace) const @@ -1778,7 +1778,7 @@ GlyphArrangement GlyphArrangementArray::getGlyphs(int const index, return glyphs; } -void GlyphArrangementArray::ensureValid(int const index) const +inline void GlyphArrangementArray::ensureValid(int const index) const { if (!isPositiveAndBelow(index, lines.size())) return; @@ -1793,7 +1793,7 @@ void GlyphArrangementArray::ensureValid(int const index) const } } -void GlyphArrangementArray::invalidateAll() +inline void GlyphArrangementArray::invalidateAll() { for (auto& entry : lines) { entry.glyphsAreDirty = true; @@ -1801,7 +1801,7 @@ void GlyphArrangementArray::invalidateAll() } } -void TextDocument::setMaximumLineWidth(int maxWidth, float viewScaleFactor) +inline void TextDocument::setMaximumLineWidth(int maxWidth, float viewScaleFactor) { maxCharWidth = (maxWidth - 12 - (48.f * viewScaleFactor)) / (7.052f * viewScaleFactor); auto caretOffset = getCaretOffset(); @@ -1810,7 +1810,7 @@ void TextDocument::setMaximumLineWidth(int maxWidth, float viewScaleFactor) setCaretOffset(caretOffset); } -SmallArray TextDocument::breakLine(String line) +inline SmallArray TextDocument::breakLine(String line) { SmallArray lines; @@ -1826,7 +1826,7 @@ SmallArray TextDocument::breakLine(String line) return lines; } -void TextDocument::replaceAll(String const& content) +inline void TextDocument::replaceAll(String const& content) { lines.clear(); @@ -1837,7 +1837,7 @@ void TextDocument::replaceAll(String const& content) } } -String TextDocument::getText() const +inline String TextDocument::getText() const { String text; for (int i = 0; i < lines.size(); i++) { @@ -1850,17 +1850,17 @@ String TextDocument::getText() const return text.trimCharactersAtEnd("\n\r"); } -int TextDocument::getNumRows() const +inline int TextDocument::getNumRows() const { return lines.size(); } -int TextDocument::getNumColumns(int const row) const +inline int TextDocument::getNumColumns(int const row) const { return lines[row].length(); } -float TextDocument::getVerticalPosition(int const row, Metric const metric) const +inline float TextDocument::getVerticalPosition(int const row, Metric const metric) const { float const lineHeight = font.getHeight() * lineSpacing; float const gap = font.getHeight() * (lineSpacing - 1.f) * 0.5f; @@ -1881,12 +1881,12 @@ float TextDocument::getVerticalPosition(int const row, Metric const metric) cons } } -Point TextDocument::getPosition(Point const index, Metric const metric) const +inline Point TextDocument::getPosition(Point const index, Metric const metric) const { return { getGlyphBounds(index).getX(), getVerticalPosition(index.x, metric) }; } -SmallArray> TextDocument::getSelectionRegion(Selection const selection, Rectangle const clip) const +inline SmallArray> TextDocument::getSelectionRegion(Selection const selection, Rectangle const clip) const { SmallArray> patches; Selection const s = selection.oriented(); @@ -1918,7 +1918,7 @@ SmallArray> TextDocument::getSelectionRegion(Selection const se return patches; } -Rectangle TextDocument::getBounds() const +inline Rectangle TextDocument::getBounds() const { if (cachedBounds.isEmpty()) { auto bounds = Rectangle(); @@ -1931,7 +1931,7 @@ Rectangle TextDocument::getBounds() const return cachedBounds; } -Rectangle TextDocument::getBoundsOnRow(int const row, Range const columns) const +inline Rectangle TextDocument::getBoundsOnRow(int const row, Range const columns) const { return getGlyphsForRow(row, -1, true) .getBoundingBox(columns.getStart(), columns.getLength(), true) @@ -1939,13 +1939,13 @@ Rectangle TextDocument::getBoundsOnRow(int const row, Range const co .withBottom(getVerticalPosition(row, Metric::bottom)); } -Rectangle TextDocument::getGlyphBounds(Point index) const +inline Rectangle TextDocument::getGlyphBounds(Point index) const { index.y = jlimit(0, getNumColumns(index.x), index.y); return getBoundsOnRow(index.x, Range(index.y, index.y + 1)); } -GlyphArrangement TextDocument::getGlyphsForRow(int const row, int const token, bool const withTrailingSpace) const +inline GlyphArrangement TextDocument::getGlyphsForRow(int const row, int const token, bool const withTrailingSpace) const { return lines.getGlyphs(row, getVerticalPosition(row, Metric::baseline), @@ -1953,7 +1953,7 @@ GlyphArrangement TextDocument::getGlyphsForRow(int const row, int const token, b withTrailingSpace); } -GlyphArrangement TextDocument::findGlyphsIntersecting(Rectangle const area, int const token) const +inline GlyphArrangement TextDocument::findGlyphsIntersecting(Rectangle const area, int const token) const { auto const range = getRangeOfRowsIntersecting(area); auto rows = SmallArray(); @@ -1965,7 +1965,7 @@ GlyphArrangement TextDocument::findGlyphsIntersecting(Rectangle const are return glyphs; } -Range TextDocument::getRangeOfRowsIntersecting(Rectangle const area) const +inline Range TextDocument::getRangeOfRowsIntersecting(Rectangle const area) const { auto const lineHeight = font.getHeight() * lineSpacing; auto row0 = jlimit(0, jmax(getNumRows() - 1, 0), static_cast(area.getY() / lineHeight)); @@ -1973,7 +1973,7 @@ Range TextDocument::getRangeOfRowsIntersecting(Rectangle const area) return { row0, row1 + 1 }; } -SmallArray TextDocument::findRowsIntersecting(Rectangle const area, +inline SmallArray TextDocument::findRowsIntersecting(Rectangle const area, bool const computeHorizontalExtent) const { auto const range = getRangeOfRowsIntersecting(area); @@ -2010,7 +2010,7 @@ SmallArray TextDocument::findRowsIntersecting(Rectangle TextDocument::findIndexNearestPosition(Point const position) const +inline Point TextDocument::findIndexNearestPosition(Point const position) const { auto const lineHeight = font.getHeight() * lineSpacing; auto row = jlimit(0, jmax(getNumRows() - 1, 0), static_cast(position.y / lineHeight)); @@ -2030,12 +2030,12 @@ Point TextDocument::findIndexNearestPosition(Point const position) c return { row, col }; } -Point TextDocument::getEnd() const +inline Point TextDocument::getEnd() const { return { getNumRows(), 0 }; } -bool TextDocument::next(Point& index) const +inline bool TextDocument::next(Point& index) const { if (index.y < getNumColumns(index.x)) { index.y += 1; @@ -2049,7 +2049,7 @@ bool TextDocument::next(Point& index) const return false; } -bool TextDocument::prev(Point& index) const +inline bool TextDocument::prev(Point& index) const { if (index.y > 0) { index.y -= 1; @@ -2063,7 +2063,7 @@ bool TextDocument::prev(Point& index) const return false; } -bool TextDocument::nextRow(Point& index) const +inline bool TextDocument::nextRow(Point& index) const { if (index.x < getNumRows()) { index.x += 1; @@ -2073,7 +2073,7 @@ bool TextDocument::nextRow(Point& index) const return false; } -bool TextDocument::prevRow(Point& index) const +inline bool TextDocument::prevRow(Point& index) const { if (index.x > 0) { index.x -= 1; @@ -2083,7 +2083,7 @@ bool TextDocument::prevRow(Point& index) const return false; } -void TextDocument::navigate(Point& i, Target const target, Direction const direction) const +inline void TextDocument::navigate(Point& i, Target const target, Direction const direction) const { std::function&)> advance; std::function&)> get; @@ -2153,7 +2153,7 @@ void TextDocument::navigate(Point& i, Target const target, Direction const } } -void TextDocument::navigateSelections(Target const target, Direction const direction, Selection::Part const part) +inline void TextDocument::navigateSelections(Target const target, Direction const direction, Selection::Part const part) { auto isHeadBeforeTail = [](Point const head, Point const tail) -> int { if (head.x == tail.x) @@ -2185,7 +2185,7 @@ void TextDocument::navigateSelections(Target const target, Direction const direc } } -void TextDocument::search(String const& text, bool clearCurrent) +inline void TextDocument::search(String const& text, bool clearCurrent) { lastSearch = text; selections.clear(); @@ -2202,7 +2202,7 @@ void TextDocument::search(String const& text, bool clearCurrent) currentSearchSelection = -1; } -juce_wchar TextDocument::getCharacter(Point const index) const +inline juce_wchar TextDocument::getCharacter(Point const index) const { jassert(0 <= index.x && index.x <= lines.size()); jassert(0 <= index.y && index.y <= lines[index.x].length()); @@ -2213,22 +2213,22 @@ juce_wchar TextDocument::getCharacter(Point const index) const return lines[index.x].getCharPointer()[index.y]; } -Selection const& TextDocument::getSelection(int const index) const +inline Selection const& TextDocument::getSelection(int const index) const { return selections[index]; } -SmallArray const& TextDocument::getSelections() const +inline SmallArray const& TextDocument::getSelections() const { return selections; } -SmallArray const& TextDocument::getSearchSelections() const +inline SmallArray const& TextDocument::getSearchSelections() const { return searchSelections; } -String TextDocument::getSelectionContent(Selection s) const +inline String TextDocument::getSelectionContent(Selection s) const { s = s.oriented(); @@ -2245,7 +2245,7 @@ String TextDocument::getSelectionContent(Selection s) const return content; } -Transaction TextDocument::fulfill(Transaction const& transaction) +inline Transaction TextDocument::fulfill(Transaction const& transaction) { cachedBounds = { }; // invalidate the bounds @@ -2285,14 +2285,14 @@ Transaction TextDocument::fulfill(Transaction const& transaction) return r; } -void TextDocument::clearTokens(Range const rows) +inline void TextDocument::clearTokens(Range const rows) { for (int n = rows.getStart(); n < rows.getEnd(); ++n) { lines.clearTokens(n); } } -void TextDocument::applyTokens(Range const rows, SmallArray const& zones) +inline void TextDocument::applyTokens(Range const rows, SmallArray const& zones) { for (int n = rows.getStart(); n < rows.getEnd(); ++n) { for (auto const& zone : zones) { @@ -2330,7 +2330,7 @@ class Transaction::Undoable final : public UndoableAction { Transaction reverse; }; -Transaction Transaction::accountingForSpecialCharacters(TextDocument const& document) const +inline Transaction Transaction::accountingForSpecialCharacters(TextDocument const& document) const { Transaction t = *this; auto& s = t.selection; @@ -2352,12 +2352,12 @@ Transaction Transaction::accountingForSpecialCharacters(TextDocument const& docu return t; } -UndoableAction* Transaction::on(TextDocument& document, Callback callback) +inline UndoableAction* Transaction::on(TextDocument& document, Callback callback) { return new Undoable(document, std::move(callback), *this); } -PlugDataTextEditor::PlugDataTextEditor() +inline PlugDataTextEditor::PlugDataTextEditor() : caret(document) , gutter(document) , highlight(document) @@ -2380,37 +2380,37 @@ PlugDataTextEditor::PlugDataTextEditor() lookAndFeelChanged(); } -void PlugDataTextEditor::lookAndFeelChanged() +inline void PlugDataTextEditor::lookAndFeelChanged() { highlight.setHighlightColour(findColour(CodeEditorComponent::highlightColourId)); searchHighlight.setHighlightColour(Colours::yellow.withAlpha(0.5f)); } -void PlugDataTextEditor::paintOverChildren(Graphics& g) +inline void PlugDataTextEditor::paintOverChildren(Graphics& g) { g.setColour(PlugDataColours::toolbarOutlineColour); g.drawHorizontalLine(0, 0, getWidth()); g.drawHorizontalLine(getHeight() - 1, 0, getWidth()); } -void PlugDataTextEditor::setFont(Font const& font) +inline void PlugDataTextEditor::setFont(Font const& font) { document.setFont(font); repaint(); } -void PlugDataTextEditor::setText(String const& text) +inline void PlugDataTextEditor::setText(String const& text) { document.replaceAll(text); repaint(); } -String PlugDataTextEditor::getText() const +inline String PlugDataTextEditor::getText() const { return document.getText(); } -void PlugDataTextEditor::translateView(float const dy) +inline void PlugDataTextEditor::translateView(float const dy) { auto const H = viewScaleFactor * document.getBounds().getHeight(); @@ -2420,7 +2420,7 @@ void PlugDataTextEditor::translateView(float const dy) updateViewTransform(); } -bool PlugDataTextEditor::scaleView(float const scaleFactor, float const verticalCenter, bool absolute) +inline bool PlugDataTextEditor::scaleView(float const scaleFactor, float const verticalCenter, bool absolute) { auto const oldS = viewScaleFactor; auto targetScale = absolute ? scaleFactor : viewScaleFactor * scaleFactor; @@ -2444,7 +2444,7 @@ bool PlugDataTextEditor::scaleView(float const scaleFactor, float const vertical return false; } -void PlugDataTextEditor::updateViewTransform() +inline void PlugDataTextEditor::updateViewTransform() { transform = AffineTransform::scale(viewScaleFactor).translated(translation.x, translation.y); highlight.setViewTransform(transform, document.getSelections()); @@ -2454,7 +2454,7 @@ void PlugDataTextEditor::updateViewTransform() repaint(); } -void PlugDataTextEditor::updateSelections() +inline void PlugDataTextEditor::updateSelections() { highlight.updateSelections(document.getSelections()); searchHighlight.updateSelections(document.getSearchSelections(), document.getCurrentSearchSelection().first); @@ -2464,7 +2464,7 @@ void PlugDataTextEditor::updateSelections() parent->repaint(); } -void PlugDataTextEditor::translateToEnsureCaretIsVisible() +inline void PlugDataTextEditor::translateToEnsureCaretIsVisible() { auto const i = document.getSelections().back().head; auto const t = Point(0.f, document.getVerticalPosition(i.x, TextDocument::Metric::top)).transformedBy(transform); @@ -2477,7 +2477,7 @@ void PlugDataTextEditor::translateToEnsureCaretIsVisible() } } -void PlugDataTextEditor::translateToEnsureSearchIsVisible(int const index) +inline void PlugDataTextEditor::translateToEnsureSearchIsVisible(int const index) { auto selections = document.getSearchSelections(); if (index >= selections.size()) @@ -2494,7 +2494,7 @@ void PlugDataTextEditor::translateToEnsureSearchIsVisible(int const index) } } -void PlugDataTextEditor::resized() +inline void PlugDataTextEditor::resized() { highlight.setBounds(getLocalBounds()); searchHighlight.setBounds(getLocalBounds()); @@ -2504,7 +2504,7 @@ void PlugDataTextEditor::resized() updateSelections(); } -void PlugDataTextEditor::paint(Graphics& g) +inline void PlugDataTextEditor::paint(Graphics& g) { g.fillAll(PlugDataColours::canvasBackgroundColour); @@ -2588,7 +2588,7 @@ void PlugDataTextEditor::paint(Graphics& g) } } -Rectangle PlugDataTextEditor::getScrollBarBounds() const +inline Rectangle PlugDataTextEditor::getScrollBarBounds() const { auto const contentHeight = document.getHeight(); auto const visibleHeight = getHeight(); @@ -2602,7 +2602,7 @@ Rectangle PlugDataTextEditor::getScrollBarBounds() const return { getWidth() - 10.f, scrollbarPosition + 2, 8.0f, scrollbarHeight - 4 }; } -void PlugDataTextEditor::mouseDown(MouseEvent const& e) +inline void PlugDataTextEditor::mouseDown(MouseEvent const& e) { if (e.getNumberOfClicks() > 1) { return; @@ -2651,7 +2651,7 @@ void PlugDataTextEditor::mouseDown(MouseEvent const& e) updateSelections(); } -void PlugDataTextEditor::mouseDrag(MouseEvent const& e) +inline void PlugDataTextEditor::mouseDrag(MouseEvent const& e) { // Check if the drag is happening within the scrollbar area (right 10px of the editor) if (e.getMouseDownX() > getWidth() - 10 && document.getHeight() > getHeight()) { @@ -2668,13 +2668,13 @@ void PlugDataTextEditor::mouseDrag(MouseEvent const& e) } } -void PlugDataTextEditor::mouseUp(MouseEvent const& e) +inline void PlugDataTextEditor::mouseUp(MouseEvent const& e) { scrollBarClicked = false; repaint(); } -void PlugDataTextEditor::mouseMove(MouseEvent const& e) +inline void PlugDataTextEditor::mouseMove(MouseEvent const& e) { if (e.x > getWidth() - 10 && document.getHeight() > getHeight() && !isOverScrollBar) { isOverScrollBar = true; @@ -2685,14 +2685,14 @@ void PlugDataTextEditor::mouseMove(MouseEvent const& e) } } -void PlugDataTextEditor::mouseExit(MouseEvent const& e) +inline void PlugDataTextEditor::mouseExit(MouseEvent const& e) { if (isOverScrollBar) { isOverScrollBar = false; growAnimator.start(); } } -void PlugDataTextEditor::mouseDoubleClick(MouseEvent const& e) +inline void PlugDataTextEditor::mouseDoubleClick(MouseEvent const& e) { if (e.getNumberOfClicks() == 2) { document.navigateSelections(TextDocument::Target::whitespace, TextDocument::Direction::backwardCol, Selection::Part::head); @@ -2706,7 +2706,7 @@ void PlugDataTextEditor::mouseDoubleClick(MouseEvent const& e) updateSelections(); } -void PlugDataTextEditor::mouseWheelMove(MouseEvent const& e, MouseWheelDetails const& d) +inline void PlugDataTextEditor::mouseWheelMove(MouseEvent const& e, MouseWheelDetails const& d) { if (e.mods.isCommandDown()) { magnifyScaleFactor = std::clamp(magnifyScaleFactor * 1.0f + d.deltaY, 0.8f, 1.2f); @@ -2719,7 +2719,7 @@ void PlugDataTextEditor::mouseWheelMove(MouseEvent const& e, MouseWheelDetails c translateView(d.deltaY * 800); } -void PlugDataTextEditor::mouseMagnify(MouseEvent const& e, float const scaleFactor) +inline void PlugDataTextEditor::mouseMagnify(MouseEvent const& e, float const scaleFactor) { magnifyScaleFactor = std::clamp(magnifyScaleFactor * (((scaleFactor - 1.0f) * 0.8f) + 1.0f), 0.8f, 1.2f); if (scaleView(magnifyScaleFactor, e.position.y)) { @@ -2727,7 +2727,7 @@ void PlugDataTextEditor::mouseMagnify(MouseEvent const& e, float const scaleFact } } -bool PlugDataTextEditor::keyPressed(KeyPress const& key) +inline bool PlugDataTextEditor::keyPressed(KeyPress const& key) { using Target = TextDocument::Target; using Direction = TextDocument::Direction; @@ -2851,7 +2851,7 @@ bool PlugDataTextEditor::keyPressed(KeyPress const& key) return false; } -bool PlugDataTextEditor::insert(String const& content) +inline bool PlugDataTextEditor::insert(String const& content) { double const now = Time::getApproximateMillisecondCounter(); @@ -2888,7 +2888,7 @@ bool PlugDataTextEditor::insert(String const& content) return true; } -MouseCursor PlugDataTextEditor::getMouseCursor() +inline MouseCursor PlugDataTextEditor::getMouseCursor() { if (isOverScrollBar) return MouseCursor::NormalCursor; @@ -2896,7 +2896,7 @@ MouseCursor PlugDataTextEditor::getMouseCursor() return getMouseXYRelative().x < (48.f * viewScaleFactor) && getMouseXYRelative().x > getWidth() - 10 ? MouseCursor::NormalCursor : MouseCursor::IBeamCursor; } -CodeEditorComponent::ColourScheme PlugDataTextEditor::getSyntaxColourScheme() +inline CodeEditorComponent::ColourScheme PlugDataTextEditor::getSyntaxColourScheme() { auto const textColour = PlugDataColours::canvasTextColour; if (PlugDataColours::canvasBackgroundColour.getPerceivedBrightness() > 0.5f) { @@ -2941,7 +2941,7 @@ CodeEditorComponent::ColourScheme PlugDataTextEditor::getSyntaxColourScheme() return cs; } -void PlugDataTextEditor::setSearchText(String const& searchText) +inline void PlugDataTextEditor::setSearchText(String const& searchText) { document.search(searchText, true); updateSelections(); @@ -2950,7 +2950,7 @@ void PlugDataTextEditor::setSearchText(String const& searchText) } } -void PlugDataTextEditor::searchNext() +inline void PlugDataTextEditor::searchNext() { auto const next = document.searchNext(); updateSelections(); @@ -3006,7 +3006,7 @@ struct TextEditorDialog final : public Component setVisible(true); // Position in centre of screen - setBounds((Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea / desktopScale).withSizeKeepingCentre(700, 500)); + setBounds((Desktop::getInstance().getDisplays().getPrimaryDisplay()->userBounds / desktopScale).withSizeKeepingCentre(700, 500).getSmallestIntegerContainer()); addAndMakeVisible(saveButton); addAndMakeVisible(undoButton); diff --git a/Source/Heavy/DaisyExporter.h b/Source/Heavy/DaisyExporter.h index 4cb2efa740..56f8e73f2b 100644 --- a/Source/Heavy/DaisyExporter.h +++ b/Source/Heavy/DaisyExporter.h @@ -90,8 +90,9 @@ class DaisyExporter final : public ExporterBase { exportingView->showState(result ? ExportingProgressView::BootloaderFlashFailure : ExportingProgressView::BootloaderFlashSuccess); exportingView->stopMonitoring(); - MessageManager::callAsync([this] { - repaint(); + MessageManager::callAsync([_this = SafePointer(this)] { + if (_this) + _this->repaint(); }); }); }; diff --git a/Source/Heavy/ExporterBase.h b/Source/Heavy/ExporterBase.h index 88804f8ced..0d6096528b 100644 --- a/Source/Heavy/ExporterBase.h +++ b/Source/Heavy/ExporterBase.h @@ -287,8 +287,9 @@ struct ExporterBase : public Component exportingView->stopMonitoring(); - MessageManager::callAsync([this] { - repaint(); + MessageManager::callAsync([_this = SafePointer(this)] { + if (_this) + _this->repaint(); }); FileSystemWatcher::removeGlobalIgnorePath(outPath); diff --git a/Source/Heavy/ExportingProgressView.h b/Source/Heavy/ExportingProgressView.h index fd9f4b9228..f5fc9a5794 100644 --- a/Source/Heavy/ExportingProgressView.h +++ b/Source/Heavy/ExportingProgressView.h @@ -585,17 +585,19 @@ class ExportingProgressView final : public Component { state = newState; - MessageManager::callAsync([this] { - setVisible(state < NotExporting); - continueButton.setVisible(state >= Success); - if (state == Exporting || state == Flashing) - console.clear(); - if (console.isShowing()) { - console.grabKeyboardFocus(); + MessageManager::callAsync([_this = SafePointer(this)] { + if (!_this) + return; + _this->setVisible(_this->state < NotExporting); + _this->continueButton.setVisible(_this->state >= Success); + if (_this->state == Exporting || _this->state == Flashing) + _this->console.clear(); + if (_this->console.isShowing()) { + _this->console.grabKeyboardFocus(); } - resized(); - repaint(); + _this->resized(); + _this->repaint(); }); } diff --git a/Source/Heavy/ToolchainInstaller.h b/Source/Heavy/ToolchainInstaller.h index 85f4990c30..ac6a8c97bf 100644 --- a/Source/Heavy/ToolchainInstaller.h +++ b/Source/Heavy/ToolchainInstaller.h @@ -196,11 +196,13 @@ class ToolchainInstaller final : public Component auto success = Decompress::extractTarXz((uint8_t const*)toolchainData.getData(), toolchainData.getSize(), toolchainDir.getParentDirectory(), expectedSize); if (!success || statusCode >= 400) { - MessageManager::callAsync([this] { - installButton.topText = "Try Again"; - errorMessage = "Error: Could not extract downloaded package"; - repaint(); - stopTimer(); + MessageManager::callAsync([_this = SafePointer(this)] { + if (!_this) + return; + _this->installButton.topText = "Try Again"; + _this->errorMessage = "Error: Could not extract downloaded package"; + _this->repaint(); + _this->stopTimer(); }); return; } @@ -210,8 +212,9 @@ class ToolchainInstaller final : public Component File driverSpec = toolchainDir.getChildFile("etc").getChildFile("usb_driver").getChildFile("DFU_in_FS_Mode.inf"); // Since we interact with ComponentPeer, better call it from the message thread - MessageManager::callAsync([this, usbDriverInstaller, driverSpec]() mutable { - OSUtils::runAsAdmin(usbDriverInstaller.getFullPathName().toStdString(), ("install --inf=" + driverSpec.getFullPathName()).toStdString(), editor->getPeer()); + MessageManager::callAsync([_this = SafePointer(this), usbDriverInstaller, driverSpec]() mutable { + if (_this) + OSUtils::runAsAdmin(usbDriverInstaller.getFullPathName().toStdString(), ("install --inf=" + driverSpec.getFullPathName()).toStdString(), _this->editor->getPeer()); }); #endif @@ -239,9 +242,11 @@ class ToolchainInstaller final : public Component installProgress = 0.0f; stopTimer(); - MessageManager::callAsync([this] { - dialog->setBlockFromClosing(false); - toolchainInstalledCallback(); + MessageManager::callAsync([_this = SafePointer(this)] { + if (!_this) + return; + _this->dialog->setBlockFromClosing(false); + _this->toolchainInstalledCallback(); }); } diff --git a/Source/Iolet.cpp b/Source/Iolet.cpp index 19d61fa9d2..ae12d3d92b 100644 --- a/Source/Iolet.cpp +++ b/Source/Iolet.cpp @@ -7,7 +7,7 @@ #include using namespace juce::gl; -#include +#include #include "Utility/Config.h" #include "Utility/Fonts.h" #include "Utility/NVGUtils.h" @@ -85,7 +85,7 @@ void Iolet::render(NVGcontext* nvg) if (isHovering) iB.expand(1.0f, 1.0f); - nvgDrawRoundedRect(nvg, iB.getX(), iB.getY(), iB.getWidth(), iB.getHeight(), innerCol, nvgColour(PlugDataColours::objectOutlineColour), PlugDataLook::useSquareIolets ? 0.0f : iB.getWidth() * 0.5f); + nanovg::nvgDrawRoundedRect(nvg, iB.getX(), iB.getY(), iB.getWidth(), iB.getHeight(), innerCol, nvgColour(PlugDataColours::objectOutlineColour), PlugDataLook::useSquareIolets ? 0.0f : iB.getWidth() * 0.5f); } bool Iolet::hitTest(int const x, int const y) diff --git a/Source/LookAndFeel.cpp b/Source/LookAndFeel.cpp index 71536c22e4..9f154401e1 100644 --- a/Source/LookAndFeel.cpp +++ b/Source/LookAndFeel.cpp @@ -729,8 +729,6 @@ void PlugDataLook::setColours(UnorderedMap& colours) colours.at(PlugDataColour::panelActiveBackgroundColourId)); setColour(TextButton::textColourOnId, colours.at(PlugDataColour::toolbarTextColourId)); - setColour(Slider::thumbColourId, - colours.at(PlugDataColour::levelMeterThumbColourId)); setColour(ScrollBar::thumbColourId, colours.at(PlugDataColour::scrollbarThumbColourId)); setColour(DirectoryContentsDisplayComponent::highlightColourId, @@ -751,6 +749,14 @@ void PlugDataLook::setColours(UnorderedMap& colours) getCurrentColourScheme().setUIColour(ColourScheme::UIColour::widgetBackground, colours.at(PlugDataColour::panelBackgroundColourId)); + + setColour(Slider::backgroundColourId, + colours.at(PlugDataColour::panelForegroundColourId)); + setColour(Slider::trackColourId, + colours.at(PlugDataColour::panelActiveBackgroundColourId).contrasting(0.15f)); + setColour(Slider::thumbColourId, + colours.at(PlugDataColour::panelActiveBackgroundColourId).contrasting(0.5f)); + setColour(TooltipWindow::backgroundColourId, colours.at(PlugDataColour::panelBackgroundColourId)); @@ -762,10 +768,6 @@ void PlugDataLook::setColours(UnorderedMap& colours) colours.at(PlugDataColour::canvasBackgroundColourId)); setColour(ScrollBar::backgroundColourId, colours.at(PlugDataColour::canvasBackgroundColourId)); - setColour(Slider::backgroundColourId, - colours.at(PlugDataColour::canvasBackgroundColourId)); - setColour(Slider::trackColourId, - colours.at(PlugDataColour::levelMeterBackgroundColourId)); setColour(TextEditor::backgroundColourId, colours.at(PlugDataColour::canvasBackgroundColourId)); setColour(FileBrowserComponent::currentPathBoxBackgroundColourId, @@ -1155,13 +1157,13 @@ const String PlugDataLook::defaultThemesJSON = R"( }, { "name": "light", - "toolbar_background": "ffebebeb", + "toolbar_background": "fff1f1f1", "toolbar_text": "ff373737", "toolbar_active": "ff007aff", - "toolbar_hover": "ffe0e0e0", + "toolbar_hover": "ffe6e6e6", "tabbar_background": "ffebebeb", "tab_text": "ff373737", - "selected_tab_background": "ffe0e0e0", + "selected_tab_background": "ffffffff", "selected_tab_text": "ff373737", "canvas_background": "fffafafa", "canvas_text": "ff4d4d4d", @@ -1178,18 +1180,18 @@ const String PlugDataLook::defaultThemesJSON = R"( "signal_colour": "ffff8500", "gem_colour": "ff01de00", "dialog_background": "ffebebeb", - "sidebar_colour": "ffefefef", + "sidebar_colour": "fff4f4f4", "sidebar_text": "ff373737", - "sidebar_background_active": "ffe6e6e6", + "sidebar_background_active": "ffebebeb", "levelmeter_active": "ff007aff", - "levelmeter_background": "ffe1e1e1", - "levelmeter_thumb": "ff9a9a9a", + "levelmeter_background": "ffe6e6e6", + "levelmeter_thumb": "ff8e8e8e", "panel_background": "fff7f7f7", "panel_foreground": "fffdfdfd", "panel_text": "ff373737", "panel_background_active": "ffececec", - "popup_background": "ffe8e8e8", - "popup_background_active": "ffdcdcdc", + "popup_background": "ffececec", + "popup_background_active": "ffe0e0e0", "popup_text": "ff373737", "scrollbar_thumb": "ffa9a9a9", "graph_area": "ffff0000", @@ -1390,9 +1392,6 @@ void PlugDataLook::setTheme(DynamicObject::Ptr themeTree) PlugDataColours::sidebarBackgroundColour = colours[PlugDataColour::sidebarBackgroundColourId]; PlugDataColours::sidebarTextColour = colours[PlugDataColour::sidebarTextColourId]; PlugDataColours::sidebarActiveBackgroundColour = colours[PlugDataColour::sidebarActiveBackgroundColourId]; - PlugDataColours::levelMeterActiveColour = colours[PlugDataColour::levelMeterActiveColourId]; - PlugDataColours::levelMeterBackgroundColour = colours[PlugDataColour::levelMeterBackgroundColourId]; - PlugDataColours::levelMeterThumbColour = colours[PlugDataColour::levelMeterThumbColourId]; PlugDataColours::panelBackgroundColour = colours[PlugDataColour::panelBackgroundColourId]; PlugDataColours::panelForegroundColour = colours[PlugDataColour::panelForegroundColourId]; PlugDataColours::panelTextColour = colours[PlugDataColour::panelTextColourId]; diff --git a/Source/LookAndFeel.h b/Source/LookAndFeel.h index 1aeba17ac0..1e75a445c1 100644 --- a/Source/LookAndFeel.h +++ b/Source/LookAndFeel.h @@ -7,7 +7,7 @@ #pragma once #include -#include +#include #include "Utility/Config.h" #include "Utility/Fonts.h" #include "Utility/SettingsFile.h" @@ -41,9 +41,6 @@ struct PlugDataColours { static inline Colour sidebarBackgroundColour; static inline Colour sidebarTextColour; static inline Colour sidebarActiveBackgroundColour; - static inline Colour levelMeterActiveColour; - static inline Colour levelMeterBackgroundColour; - static inline Colour levelMeterThumbColour; static inline Colour panelBackgroundColour; static inline Colour panelForegroundColour; static inline Colour panelTextColour; @@ -100,10 +97,6 @@ inline UnorderedMap> const Pl { toolbarOutlineColourId, { "Outline", "toolbar_outline_colour", "Other" } }, { scrollbarThumbColourId, { "Scrollbar thumb", "scrollbar_thumb", "Other" } }, - { levelMeterActiveColourId, { "Level meter active", "levelmeter_active", "Level Meter" } }, - { levelMeterBackgroundColourId, { "Level meter track", "levelmeter_background", "Level Meter" } }, - { levelMeterThumbColourId, { "Level meter thumb", "levelmeter_thumb", "Level Meter" } }, - { panelBackgroundColourId, { "Panel background", "panel_background", "Properties Panel" } }, { panelForegroundColourId, { "Panel foreground", "panel_foreground", "Properties Panel" } }, { panelTextColourId, { "Panel text", "panel_text", "Properties Panel" } }, diff --git a/Source/NVGSurface.cpp b/Source/NVGSurface.cpp index 859066a6f5..520a812ad0 100644 --- a/Source/NVGSurface.cpp +++ b/Source/NVGSurface.cpp @@ -5,116 +5,69 @@ */ #include -#include -#include - +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) +# include +#else +# include using namespace juce::gl; +#endif +#include -#include -#ifdef NANOVG_GL_IMPLEMENTATION +#include +#ifndef NANOVG_METAL_IMPLEMENTATION # include -# include # if JUCE_LINUX || JUCE_BSD -void nvgluSetCornerRadius(float radius, bool left, bool right); +void nvgSetCornerRadius(float radius, bool left, bool right); # endif #endif #include "NVGSurface.h" #include "Utility/NVGGraphicsContext.h" +#include "Utility/OSUtils.h" #include "PluginEditor.h" #include "PluginProcessor.h" -#define ENABLE_FPS_COUNT 0 - -class FrameTimer { -public: - FrameTimer() - { - startTime = getNow(); - prevTime = startTime; - } - - void render(NVGcontext* nvg, int const width, int const height, float const scale) - { - nvgBeginFrame(nvg, width, height, scale); - - nvgFillColor(nvg, nvgRGBA(40, 40, 40, 255)); - nvgFillRect(nvg, 0, 0, 40, 22); - - nvgFontSize(nvg, 20.0f); - nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); - nvgFillColor(nvg, nvgRGBA(240, 240, 240, 255)); - StackArray fpsBuf; - snprintf(fpsBuf.data(), 16, "%d", static_cast(round(1.0f / getAverageFrameTime()))); - nvgText(nvg, 7, 2, fpsBuf.data(), nullptr); - - nvgGlobalScissor(nvg, 0, 0, 40 * scale, 22 * scale); - nvgEndFrame(nvg); - } - void addFrameTime() - { - auto const timeSeconds = getTime(); - auto const dt = timeSeconds - prevTime; - perf_head = (perf_head + 1) % 32; - frame_times[perf_head] = dt; - prevTime = timeSeconds; - } - - double getTime() const { return getNow() - startTime; } - -private: - static double getNow() - { - auto const ticks = Time::getHighResolutionTicks(); - return Time::highResolutionTicksToSeconds(ticks); - } - - float getAverageFrameTime() const - { - float avg = 0; - for (int i = 0; i < 32; i++) { - avg += frame_times[i]; - } - return avg / static_cast(32); - } - - float frame_times[32] = { }; - int perf_head = 0; - double startTime = 0, prevTime = 0; -}; - NVGSurface::NVGSurface(PluginEditor* e) - : editor(e) + : +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + Thread("NVGSurface Metal Renderer"), +#endif + editor(e) { #ifdef NANOVG_GL_IMPLEMENTATION glContext = std::make_unique(); + glContext->setRenderer(this); + glContext->setComponentPaintingEnabled(false); auto pixelFormat = OpenGLPixelFormat(8, 8, 16, 8); glContext->setPixelFormat(pixelFormat); # ifdef NANOVG_GL3_IMPLEMENTATION - glContext->setOpenGLVersionRequired(OpenGLContext::OpenGLVersion::openGL3_2); + glContext->setPreferredProfile(OpenGLProfile::core); + glContext->setPreferredVersion(OpenGLVersion(3, 2)); # endif glContext->setSwapInterval(0); + glContext->setContinuousRepainting(false); #endif -#if ENABLE_FPS_COUNT - frameTimer = std::make_unique(); +#if PLUGDATA_NVG_FRAME_TIME_OVERLAY + frameTimeOverlay = std::make_unique(); + frameTimeVBlankAttachment = std::make_unique(this, [this](double timestampSec) { + if (!frameTimeOverlay) + return; + + frameTimeOverlay->addVBlank(timestampSec); + requestBackendRender(); + }); #endif setInterceptsMouseClicks(false, false); setWantsKeyboardFocus(false); - + setVisible(true); setSize(1, 1); - backupImageComponent.setInterceptsMouseClicks(false, false); - editor->addChildComponent(backupImageComponent); - - // Start rendering asynchronously, so we are sure the window has been added to the desktop - // kind of a hack, but works well enough MessageManager::callAsync([_this = SafePointer(this)] { if (_this) { - _this->vBlankAttachment = std::make_unique(_this.getComponent(), [_this]() { - _this->render(); - }); + _this->initialise(); + _this->invalidateAll(); } }); } @@ -126,379 +79,636 @@ NVGSurface::~NVGSurface() void NVGSurface::initialise() { -#ifdef NANOVG_METAL_IMPLEMENTATION - auto* peer = getPeer()->getNativeHandle(); - auto* view = OSUtils::MTLCreateView(peer, 0, 0, getWidth(), getHeight()); - setView(view); - setVisible(true); + // Publish the editor size before the render thread can start, so its first + // createRenderContext/renderBackendFrame reads a valid snapshot. + snapshotEditorSize(); + +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + if (!metalView) { + auto* peer = getPeer(); + if (!peer) + return; - lastRenderScale = calculateRenderScale(); - nvg = nvgCreateContext(view, 0, getWidth() * lastRenderScale, getHeight() * lastRenderScale); + auto* nativePeer = peer->getNativeHandle(); + metalView = OSUtils::MTLCreateView(nativePeer, 0, 0, getWidth(), getHeight()); + metalLayer = OSUtils::MTLGetLayer(metalView); + setView(metalView); + setVisible(true); + } + + updateRenderScale(); + + if (!isThreadRunning()) { + backendRenderRequested.store(true); + startThread(Thread::Priority::high); + notify(); + } +#elif NANOVG_GL_IMPLEMENTATION + if (glContext && !glContext->isAttached()) { + glContext->attachTo(*this); + } +#endif +} + +void NVGSurface::createRenderContext() +{ + if (baseNvg) + return; + +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + // Render thread: use the message-thread snapshot, not the editor Component. + auto const pixelScale = calculateRenderScale(); + auto const viewWidth = jmax(1, roundToInt(editorWidth.load(std::memory_order_relaxed) * pixelScale)); + auto const viewHeight = jmax(1, roundToInt(editorHeight.load(std::memory_order_relaxed) * pixelScale)); + + if (!metalLayer) + return; + + baseNvg = nvgCreateContextForLayer(metalLayer, 0, viewWidth, viewHeight); +#elif NANOVG_GL_IMPLEMENTATION + // Runs on the render thread. Context create/destroy and the main framebuffer + // are render-thread-owned; no contextMutex needed (see NVGSurface.h / recordFrame). + baseNvg = nvgCreateContext(0); #else - setVisible(true); - glContext->attachTo(*this); - glContext->initialiseOnThread(); - glContext->makeActive(); - lastRenderScale = calculateRenderScale(); - nvg = nvgCreateContext(0); + return; #endif - if (!nvg) { - static bool isShowingMessageBox = false; - if (!isShowingMessageBox) { - isShowingMessageBox = true; - std::cerr << "could not initialise nvg" << std::endl; - AlertWindow::showMessageBoxAsync(MessageBoxIconType::WarningIcon, - "Could not initialize plugdata", - "Please check that you have up-to-date graphics drivers installed. At least OpenGL 3.0 support is required to run plugdata.", - "OK", - nullptr, - nullptr); - } + if (!baseNvg) { +#if !ENABLE_TESTING + MessageManager::callAsync([] { + static bool isShowingMessageBox = false; + if (!isShowingMessageBox) { + isShowingMessageBox = true; + std::cerr << "could not initialise nvg" << std::endl; +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + auto const message = "Please check that Metal is available and working on this system."; +#else + auto const message = "Please check that you have up-to-date graphics drivers installed. At least OpenGL 3.0 support is required to run plugdata."; +#endif + AlertWindow::showMessageBoxAsync(MessageBoxIconType::WarningIcon, + "Could not initialize plugdata", + message, + "OK", + nullptr, + nullptr); + } + }); +#endif return; } -#if JUCE_LINUX || JUCE_BSD - nvgluSetCornerRadius(12.0f * lastRenderScale, roundedRight, roundedRight); -#endif + auto* asyncNvg = nanovg::create(baseNvg, false); + nvg.store(asyncNvg); - surfaces[nvg] = this; +# if JUCE_LINUX || JUCE_BSD + nvgSetCornerRadius(12.0f * calculateRenderScale()); +# endif - nvgAtlasTextThreshold(nvg, 32.0f); - nvgCreateFontMem(nvg, "Inter-Regular", BinaryData::getResourceCopy(BinaryData::InterRegular_ttf), BinaryData::getResourceSize(BinaryData::InterRegular_ttf), 0); - nvgCreateFontMem(nvg, "Inter-Bold", BinaryData::getResourceCopy(BinaryData::InterBold_ttf), BinaryData::getResourceSize(BinaryData::InterBold_ttf), 0); - nvgCreateFontMem(nvg, "Inter-Tabular", BinaryData::getResourceCopy(BinaryData::InterTabular_ttf), BinaryData::getResourceSize(BinaryData::InterTabular_ttf), 0); - nvgCreateFontMem(nvg, "icon_font-Regular", BinaryData::getResourceCopy(BinaryData::IconFont_ttf), BinaryData::getResourceSize(BinaryData::IconFont_ttf), 0); - updateWindowContextVisibility(); + surfaces[asyncNvg] = this; + MessageManager::callAsync([_this = SafePointer(this)] { + if (_this) + _this->invalidateAll(); + }); } -void NVGSurface::updateWindowContextVisibility() +void NVGSurface::destroyRenderContext() { - if (renderThroughImage == isRenderingThroughImage) - return; + // Runs on the render thread, while the message thread is blocked in detach(), + // so recordFrame cannot be running concurrently during GL shutdown. Metal + // calls this from its render thread after stopThread() has been requested. + + // Destroy the render-thread-owned main framebuffer while its backend context + // is still current/valid (before nvgDeleteContext). + if (mainFramebuffer) { + nvgDeleteFramebuffer(reinterpret_cast(mainFramebuffer)); + mainFramebuffer = nullptr; + } + mainFramebufferWidth = 0; + mainFramebufferHeight = 0; - isRenderingThroughImage = renderThroughImage; + if (auto* asyncNvg = nvg.exchange(nullptr)) { + surfaces.erase(asyncNvg); + nanovg::destroy(asyncNvg); // frees the async layer's own backend framebuffers + } - // Render a frame to the new target before showing/hiding GPU context - renderAll(); + if (baseNvg) { + nvgDeleteContext(baseNvg); + baseNvg = nullptr; + } -#ifdef NANOVG_GL_IMPLEMENTATION - if (glContext) - glContext->setVisible(!renderThroughImage); -#else - if (auto* view = getView()) { - OSUtils::MTLSetVisible(view, !renderThroughImage); + frameReadyForReplay.store(false); + recordedFramebufferWidth.store(0); + recordedFramebufferHeight.store(0); + recordedFrameIsFullRepaint.store(false); +} + +#if NANOVG_GL_IMPLEMENTATION +void NVGSurface::newOpenGLContextCreated() +{ + glContext->setSwapInterval(0); + createRenderContext(); +} + +void NVGSurface::openGLContextClosing() +{ + destroyRenderContext(); +} +#endif + +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) +void NVGSurface::run() +{ + createRenderContext(); + + while (!threadShouldExit()) { + if (!backendRenderRequested.exchange(false)) + wait(-1); + + if (threadShouldExit()) + break; + + JUCE_AUTORELEASEPOOL + { + renderBackendFrame(); + } } + + destroyRenderContext(); +} #endif - invalidateAll(); +void NVGSurface::presentFramebuffer(int viewWidth, int viewHeight) +{ + nvgBindFramebuffer(nullptr); + +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + // Render thread: resize the drawable via the cached layer, never the view. + if (metalLayer) + mnvgSetLayerDrawableSize(metalLayer, viewWidth, viewHeight); +#endif + + nvgViewport(0, 0, viewWidth, viewHeight); + + if (mainFramebuffer) { + nvgBlitFramebuffer(baseNvg, reinterpret_cast(mainFramebuffer), 0, 0, viewWidth, viewHeight); + } else { + nvgClearWithColor(baseNvg, nvgColour(PlugDataColours::canvasBackgroundColour)); + } } -void NVGSurface::detachContext() +void NVGSurface::renderBackendFrame() { - if (makeContextActive()) { - editor->getNanoLLGC()->removeCachedImages(); - NVGFramebuffer::clearAll(nvg); - NVGImage::clearAll(nvg); - NVGCachedPath::clearAll(nvg); + auto const pixelScale = calculateRenderScale(); + auto const viewWidth = jmax(1, roundToInt(editorWidth.load(std::memory_order_relaxed) * pixelScale)); + auto const viewHeight = jmax(1, roundToInt(editorHeight.load(std::memory_order_relaxed) * pixelScale)); - if (invalidFBO) { - nvgDeleteFramebuffer(invalidFBO); - invalidFBO = nullptr; - } - if (nvg) { - nvgDeleteContext(nvg); - nvg = nullptr; - surfaces.erase(nvg); + if (!baseNvg) + return; + + auto* asyncNvg = nvg.load(); + if (!asyncNvg) + return; + + serviceReadbackRequest(); + + if (!mainFramebuffer || mainFramebufferWidth != viewWidth || mainFramebufferHeight != viewHeight) { + auto const hasMatchingFullFrame = frameReadyForReplay.load() + && nanovg::hasPendingFrame(asyncNvg) + && recordedFrameIsFullRepaint.load() + && recordedFramebufferWidth.load() == viewWidth + && recordedFramebufferHeight.load() == viewHeight; + + if (!hasMatchingFullFrame) { + MessageManager::callAsync([_this = SafePointer(this)] { + if (_this) + _this->invalidateAll(); + }); + + presentFramebuffer(viewWidth, viewHeight); + return; } -#ifdef NANOVG_METAL_IMPLEMENTATION - if (auto* view = getView()) { - OSUtils::MTLDeleteView(view); - setView(nullptr); + auto* oldFramebuffer = reinterpret_cast(mainFramebuffer); + + if (auto* newFramebuffer = nvgCreateFramebuffer(baseNvg, viewWidth, viewHeight, 0)) { + mainFramebuffer = newFramebuffer; + mainFramebufferWidth = viewWidth; + mainFramebufferHeight = viewHeight; + + if (oldFramebuffer) + nvgDeleteFramebuffer(oldFramebuffer); + } else { + presentFramebuffer(viewWidth, viewHeight); + return; } -#else - glContext->detach(); + } + + nanovg::setMainFramebuffer(asyncNvg, mainFramebuffer); + nvgBindFramebuffer(reinterpret_cast(mainFramebuffer)); + + // Replay the recorded frame: redraws the damaged region into the persistent + // framebuffer (the rest of it is preserved). +#if PLUGDATA_NVG_FRAME_TIME_OVERLAY + auto const renderStartMs = Time::getMillisecondCounterHiRes(); +#endif + bool const didRender = nanovg::performRender(asyncNvg); + + if (didRender) { + frameReadyForReplay.store(false); +#if PLUGDATA_NVG_FRAME_TIME_OVERLAY + // The render-thread cost of actually replaying the frame -- a real estimate + // of frame performance, unlike the (throttled) interval between repaints. + if (frameTimeOverlay) + frameTimeOverlay->addFrame(Time::getMillisecondCounterHiRes() - renderStartMs); #endif - isRenderingThroughImage = false; } + +#if PLUGDATA_NVG_FRAME_TIME_OVERLAY + drawFrameTimeOverlay(viewWidth, viewHeight, pixelScale); +#endif + + // Composite the persistent framebuffer onto the screen every pass, so the + // window keeps showing the accumulated content after each buffer swap. + presentFramebuffer(viewWidth, viewHeight); } -void NVGSurface::updateBufferSize() +#if NANOVG_GL_IMPLEMENTATION +void NVGSurface::renderOpenGL() { - float const pixelScale = getRenderScale(); - int const scaledWidth = std::max(getWidth() * pixelScale, 1); - int const scaledHeight = std::max(getHeight() * pixelScale, 1); + updateRenderScale(); + + renderBackendFrame(); +} +#endif + +void NVGSurface::detachContext() +{ +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + if (isThreadRunning()) { + backendRenderRequested.store(true); + stopThread(5000); + } - if (fbWidth != scaledWidth || fbHeight != scaledHeight || !invalidFBO) { - if (invalidFBO) - nvgDeleteFramebuffer(invalidFBO); - invalidFBO = nvgCreateFramebuffer(nvg, scaledWidth, scaledHeight, 0); - fbWidth = scaledWidth; - fbHeight = scaledHeight; - invalidArea = getLocalBounds(); + backendRenderRequested.store(false); + + if (metalView) { + setView(nullptr); + OSUtils::MTLDeleteView(metalView); + metalView = nullptr; + metalLayer = nullptr; // owned by the view; cleared here after the render thread has stopped } +#elif NANOVG_GL_IMPLEMENTATION + if (glContext) + glContext->detach(); +#endif } void NVGSurface::lookAndFeelChanged() { - if (makeContextActive()) { - NVGFramebuffer::clearAll(nvg); - NVGImage::clearAll(nvg); - invalidateAll(); + // Message thread. Records cache-clear ops into the async buffer; no lock + // needed (record<->replay is decoupled by the async layer's buffer swap). + if (auto* asyncNvg = nvg.load()) { + NVGCachedPath::clearAll(asyncNvg); + NVGImage::clearAll(asyncNvg); } + invalidateAll(); } bool NVGSurface::makeContextActive() { -#ifdef NANOVG_METAL_IMPLEMENTATION - // No need to make context active with Metal, so just check if we have initialised and return that - return getView() != nullptr && nvg != nullptr && mnvgDevice(nvg) != nullptr; +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + return isThreadRunning() && nvg.load() != nullptr; +#elif NANOVG_GL_IMPLEMENTATION + return glContext && glContext->isAttached(); #else - if (glContext && glContext->makeActive()) { - if (renderThroughImage) - updateWindowContextVisibility(); - return true; - } - return false; #endif } float NVGSurface::calculateRenderScale() const { -#ifdef NANOVG_METAL_IMPLEMENTATION - return OSUtils::MTLGetPixelScale(getView()) * Desktop::getInstance().getGlobalScaleFactor(); -#else - return glContext->getRenderingScale(); + auto const scale = cachedRenderScale.load(std::memory_order_relaxed); + if (scale > 0.0f) + return scale; + + return Desktop::getInstance().getGlobalScaleFactor(); +} + +void NVGSurface::updateRenderScale() +{ +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + if (metalView) { + auto const scale = OSUtils::MTLGetPixelScale(metalView); + if (scale > 0.0f) + cachedRenderScale.store(scale * Desktop::getInstance().getGlobalScaleFactor(), std::memory_order_relaxed); + } +#elif NANOVG_GL_IMPLEMENTATION + if (glContext) { + auto const scale = glContext->getRenderingScale(); + if (scale > 0.0) + cachedRenderScale.store(static_cast(scale), std::memory_order_relaxed); + } #endif } -float NVGSurface::getRenderScale() const +void NVGSurface::snapshotEditorSize() { - return lastRenderScale; + // Message thread only. Publishes the editor's logical size so the render thread + // can size the drawable/framebuffer without touching the editor Component, whose + // bounds are message-thread-only. + JUCE_ASSERT_MESSAGE_THREAD; + editorWidth.store(jmax(1, editor->getWidth()), std::memory_order_relaxed); + editorHeight.store(jmax(1, editor->getHeight()), std::memory_order_relaxed); } -void NVGSurface::updateBounds(Rectangle const bounds) +float NVGSurface::getRenderScale() const { - currentBounds = bounds; + return lastRenderScale > 0.0f ? lastRenderScale : calculateRenderScale(); +} -#if JUCE_LINUX || JUCE_BSD - // Directly setting bounds gives the best result on Linux - setBounds(bounds); -#endif +void NVGSurface::updateBounds(Rectangle) +{ + currentBounds = editor->getLocalBounds(); + snapshotEditorSize(); -#ifdef NANOVG_GL_IMPLEMENTATION - updateWindowContextVisibility(); -#endif + if (getBounds() != currentBounds) + setBounds(currentBounds); + initialise(); invalidateAll(); } void NVGSurface::resized() { -#if JUCE_LINUX || JUCE_BSD - Path clipPath; - clipPath.addRoundedRectangle(0, 0, getWidth(), getHeight(), Corners::windowCornerRadius, Corners::windowCornerRadius, roundedLeft, roundedRight, roundedLeft, roundedRight); - backupImageComponent.setClipPath(clipPath); -#endif - backupImageComponent.setBounds(editor->getLocalArea(this, getLocalBounds())); + snapshotEditorSize(); + invalidateAll(); + +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + updateRenderScale(); +#endif } void NVGSurface::invalidateAll() { - invalidArea = invalidArea.getUnion(getLocalBounds()); + invalidateArea(getLocalBounds()); } void NVGSurface::invalidateArea(Rectangle const area) { invalidArea = invalidArea.getUnion(area); + scheduleRender(); } -void NVGSurface::renderAll() +void NVGSurface::handleAsyncUpdate() { - invalidateAll(); - render(); + recordFrame(); } -void NVGSurface::render() +void NVGSurface::scheduleRender() { - if (renderThroughImage) { - auto const startTime = Time::getMillisecondCounter(); - if (startTime - lastRenderTime < 32) { - return; // When rendering through juce::image, limit framerate to 30 fps - } - lastRenderTime = startTime; - } + triggerAsyncUpdate(); +} + +void NVGSurface::requestBackendRender() +{ +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + backendRenderRequested.store(true); + notify(); +#elif NANOVG_GL_IMPLEMENTATION + if (glContext) + glContext->triggerRepaint(); +#endif +} - if (!getPeer()) { +#if PLUGDATA_NVG_FRAME_TIME_OVERLAY +void NVGSurface::drawFrameTimeOverlay(int const viewWidth, int const viewHeight, float const scale) +{ + if (!frameTimeOverlay || !baseNvg || !mainFramebuffer) return; - } - // Do this right before rendering, so that it doesn't show a frame with the last rendered content skewed to the new view size - if (getBounds() != currentBounds) { - setBounds(currentBounds); - } + auto const size = FrameTimeOverlay::sizeFor(scale); + auto const margin = 10.0f * scale; + auto const x = static_cast(viewWidth) - size.width - margin; + auto const y = static_cast(viewHeight) - size.height - margin; -#if JUCE_LINUX - if (skipFrame) { - // On Linux, there is a strange bug where repaints will be executed immediately if the last repaint took too long - // This will then completely occupy the message thread, leaving no space for handling interaction... - // So if our rendering took too long, we need to manually skip a frame - skipFrame = false; - return; - } - auto start = Time::getMillisecondCounter(); + nvgViewport(0, 0, viewWidth, viewHeight); + + nvgBeginFrame(baseNvg, static_cast(viewWidth), static_cast(viewHeight), 1.0f); + frameTimeOverlay->draw(baseNvg, x, y, scale); + nvgGlobalScissor(baseNvg, 0, 0, viewWidth, viewHeight); + nvgEndFrame(baseNvg); +} #endif - if (!nvg) { - initialise(); - if (!nvg) { - return; - } - } +void NVGSurface::recordFrame() +{ + JUCE_ASSERT_MESSAGE_THREAD; - if (!makeContextActive()) { + auto const bounds = editor->getLocalBounds(); + snapshotEditorSize(); + if (bounds.isEmpty()) return; - } - auto pixelScale = calculateRenderScale(); +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + updateRenderScale(); +#endif + + lastRenderScale = calculateRenderScale(); auto const desktopScale = Desktop::getInstance().getGlobalScaleFactor(); - auto const devicePixelScale = pixelScale / desktopScale; + auto const devicePixelScale = lastRenderScale / desktopScale; - if (std::abs(lastRenderScale - pixelScale) > 0.1f) { - detachContext(); - initialise(); - return; // Render on next frame - } + int const fbWidth = jmax(1, roundToInt(bounds.getWidth() * lastRenderScale)); + int const fbHeight = jmax(1, roundToInt(bounds.getHeight() * lastRenderScale)); -#if NANOVG_METAL_IMPLEMENTATION - if (pixelScale == 0) // This happens sometimes when an AUv3 plugin is hidden behind the parameter control view + // No contextMutex: recording only writes into the async layer's record buffer + // (handed to the render thread via publish()'s tiny-lock swap) and reads the + // `nvg` atomic. Context teardown runs on the render thread while the message + // thread is blocked in detach(), so it can never overlap this. + auto* asyncNvg = nvg.load(); + if (!asyncNvg) { + initialise(); return; -#else - auto viewWidth = getWidth() * pixelScale; - auto viewHeight = getHeight() * pixelScale; - - // In case we apply a global scale factor, calculate the necessary fractional offset for glViewport size - if (!approximatelyEqual(desktopScale, 1.0f)) { - auto desktopScaleOffsetX = getWidth() * desktopScale; - auto desktopScaleOffsetY = getHeight() * desktopScale; - viewWidth += (std::ceil(desktopScaleOffsetX) - desktopScaleOffsetX) * devicePixelScale; - viewHeight += (std::ceil(desktopScaleOffsetY) - desktopScaleOffsetY) * devicePixelScale; } -#endif -#if ENABLE_FPS_COUNT - frameTimer->addFrameTime(); +#if PLUGDATA_NVG_FRAME_TIME_OVERLAY + auto const prepareStartMs = Time::getMillisecondCounterHiRes(); #endif - updateBufferSize(); + { + // Damage tracking: draw only the region invalidated through the + // InvalidationListener. If the previously recorded frame is still pending + // it will be coalesced away by the publish below, so fold its damage back + // in to make sure nothing is lost. + if (nanovg::hasPendingFrame(asyncNvg)) + invalidArea = invalidArea.getUnion(inFlightDamage); + + auto const damage = invalidArea.getIntersection(bounds); + if (damage.isEmpty()) + return; + auto const isFullRepaint = damage.contains(bounds); - invalidArea = invalidArea.getIntersection(getLocalBounds()); + nanovg::setCurrentPixelScale(asyncNvg, devicePixelScale); - for (auto bufferedObject : bufferedObjects) { - if (bufferedObject) - bufferedObject->updateFramebuffers(nvg); - } + for (auto bufferedObject : bufferedObjects) { + if (bufferedObject) + bufferedObject->updateFramebuffers(asyncNvg); + } - if (!invalidArea.isEmpty()) { - // Draw only the invalidated region on top of framebuffer - nvgBindFramebuffer(invalidFBO); -#ifdef NANOVG_GL_IMPLEMENTATION - nvgViewport(0, 0, viewWidth, viewHeight); -#endif - nvgBeginFrame(nvg, getWidth() * desktopScale, getHeight() * desktopScale, devicePixelScale); - nvgScale(nvg, desktopScale, desktopScale); - editor->renderArea(nvg, invalidArea); - nvgGlobalScissor(nvg, invalidArea.getX() * pixelScale, invalidArea.getY() * pixelScale, invalidArea.getWidth() * pixelScale, invalidArea.getHeight() * pixelScale); - nvgEndFrame(nvg); - -#if ENABLE_FPS_COUNT - frameTimer->render(nvg, getWidth(), getHeight(), pixelScale); + // Render the damaged region into the persistent main framebuffer. The + // render thread owns that framebuffer; we just record "bind the main target" + // here. It must come AFTER updateFramebuffers, because buffered objects + // bind (and unbind to the default target) their own framebuffers while + // updating. + nanovg::bindMainFramebuffer(asyncNvg); + nanovg::viewport(asyncNvg, 0, 0, fbWidth, fbHeight); + + if (isFullRepaint) + nanovg::clear(asyncNvg); + + nanovg::nvgBeginFrame(asyncNvg, bounds.getWidth() * desktopScale, bounds.getHeight() * desktopScale, devicePixelScale); + nanovg::nvgScale(asyncNvg, desktopScale, desktopScale); + + auto& llgc = editor->getOrCreateNanoLLGC(asyncNvg, lastRenderScale); + llgc.resetClipRegion(AffineTransform::scale(desktopScale, desktopScale)); + Graphics g(llgc); + g.reduceClipRegion(invalidArea); + + editor->paintEntireComponent(g, false); + +#if PLUGDATA_NVG_REPAINT_DEBUG + static Random rng; + g.fillAll (Colour ((uint8) rng.nextInt (255), + (uint8) rng.nextInt (255), + (uint8) rng.nextInt (255), + (uint8) 0x50)); #endif - if (renderThroughImage) { - renderFrameToImage(backupRenderImage, invalidArea); - } else { - needsBufferSwap = true; - } - invalidArea = Rectangle(0, 0, 0, 0); - } - - if (needsBufferSwap) { - blitToScreen(); - needsBufferSwap = false; + // Restrict the backend flush to the damaged region so the rest of the + // framebuffer keeps its previous contents (persistence). Coordinates are + // in device pixels. + nanovg::nvgGlobalScissor(asyncNvg, + roundToInt(damage.getX() * lastRenderScale), + roundToInt(damage.getY() * lastRenderScale), + roundToInt(damage.getWidth() * lastRenderScale), + roundToInt(damage.getHeight() * lastRenderScale)); + + nanovg::nvgEndFrame(asyncNvg); + + inFlightDamage = damage; + recordedFramebufferWidth.store(fbWidth); + recordedFramebufferHeight.store(fbHeight); + recordedFrameIsFullRepaint.store(isFullRepaint); } -#if JUCE_LINUX - auto* display = Desktop::getInstance().getDisplays().getPrimaryDisplay(); - auto refreshRate = display ? display->verticalFrequencyHz.value_or(60.0) : 60.0; - if (Time::getMillisecondCounter() - start > (1000 / refreshRate)) { - skipFrame = true; - } +#if PLUGDATA_NVG_FRAME_TIME_OVERLAY + if (frameTimeOverlay) + frameTimeOverlay->setPrepareTime(Time::getMillisecondCounterHiRes() - prepareStartMs); #endif + + invalidArea = {}; + frameReadyForReplay.store(true); + requestBackendRender(); } -void NVGSurface::blitToScreen() +void NVGSurface::serviceReadbackRequest() { - if (!makeContextActive() || !invalidFBO) { + // Render thread. Reads the requested region straight out of the persistent + // main framebuffer and hands the raw BGRA pixels back to the waiting caller. + if (!readbackPending.load()) return; - } - auto pixelScale = calculateRenderScale(); + auto const area = readbackDeviceArea; + bool succeeded = false; -#if NANOVG_METAL_IMPLEMENTATION - auto const devicePixelScale = pixelScale / Desktop::getInstance().getGlobalScaleFactor(); - auto viewWidth = getWidth() * devicePixelScale; - auto viewHeight = getHeight() * devicePixelScale; - if (auto* view = getView()) { - mnvgSetViewBounds(view, getWidth() * pixelScale, getHeight() * pixelScale); - } -#else - auto viewWidth = getWidth() * pixelScale; - auto viewHeight = getHeight() * pixelScale; -#endif + if (baseNvg && mainFramebuffer && !area.isEmpty() + && area.getX() >= 0 && area.getY() >= 0 + && area.getRight() <= mainFramebufferWidth + && area.getBottom() <= mainFramebufferHeight) { + auto const w = area.getWidth(); + auto const h = area.getHeight(); - nvgBindFramebuffer(nullptr); - nvgBlitFramebuffer(nvg, invalidFBO, 0, 0, viewWidth, viewHeight); + readbackData.malloc(static_cast(w) * h * 4); + nvgReadPixels(baseNvg, reinterpret_cast(mainFramebuffer), + area.getX(), area.getY(), w, h, mainFramebufferHeight, readbackData.getData()); -#ifdef NANOVG_GL_IMPLEMENTATION - glContext->swapBuffers(); -#endif -} + readbackWidth = w; + readbackHeight = h; + succeeded = true; + } -#if JUCE_LINUX || JUCE_BSD -void NVGSurface::setRoundedBottomCorners(bool left, bool right) -{ - roundedLeft = left; - roundedRight = right; - nvgluSetCornerRadius(12.0f * lastRenderScale, roundedLeft, roundedRight); + readbackSucceeded = succeeded; + readbackPending.store(false); + readbackReady.signal(); } -#endif -void NVGSurface::renderFrameToImage(Image& image, Rectangle const area) +Image NVGSurface::renderToImage(Rectangle logicalArea) { - nvgBindFramebuffer(nullptr); + JUCE_ASSERT_MESSAGE_THREAD; + + if (!makeContextActive()) + return {}; + + logicalArea = logicalArea.getIntersection(getLocalBounds()); + if (logicalArea.isEmpty()) + return {}; + + auto const scale = getRenderScale(); + Rectangle const deviceBounds(0, 0, + jmax(1, roundToInt(getWidth() * scale)), + jmax(1, roundToInt(getHeight() * scale))); - if (!image.isValid() || image.getWidth() != fbWidth || image.getHeight() != fbHeight) { - image = Image(Image::PixelFormat::ARGB, fbWidth, fbHeight, true); + auto const deviceArea = Rectangle( + roundToInt(logicalArea.getX() * scale), + roundToInt(logicalArea.getY() * scale), + jmax(1, roundToInt(logicalArea.getWidth() * scale)), + jmax(1, roundToInt(logicalArea.getHeight() * scale))) + .getIntersection(deviceBounds); + + if (deviceArea.isEmpty()) + return {}; + + // One readback at a time; the eyedropper is the only caller. + ScopedLock const sl(readbackLock); + + readbackDeviceArea = deviceArea; + readbackSucceeded = false; + readbackReady.reset(); + readbackPending.store(true); + + // Kick the render thread, which owns the framebuffer, to service the request. + requestBackendRender(); + + if (!readbackReady.wait(200) || !readbackSucceeded) { + readbackPending.store(false); + return {}; } - Image::BitmapData imageData(image, Image::BitmapData::writeOnly); - auto region = (area.getIntersection(getLocalBounds()).toFloat() * getRenderScale()).getSmallestIntegerContainer().getIntersection({ fbWidth, fbHeight }); - nvgReadPixels(nvg, invalidFBO, 0, region.getY(), fbWidth, region.getHeight(), fbHeight, imageData.getLinePointer(region.getY())); + auto const w = readbackWidth; + auto const h = readbackHeight; - backupImageComponent.setImage(image); - backupImageComponent.repaint(area); -} + Image image(Image::ARGB, w, h, false); + { + Image::BitmapData bitmap(image, Image::BitmapData::writeOnly); + auto const* src = readbackData.getData(); + for (int y = 0; y < h; ++y) { + // nvgReadPixels returns top-left-origin BGRA for both GL and Metal. + auto const* s = src + static_cast(y) * w * 4; + auto* d = reinterpret_cast(bitmap.getLinePointer(y)); + for (int x = 0; x < w; ++x) { + // Force opaque: the eyedropper wants the visible colour, not the + // framebuffer's alpha. + d[x].setARGB(255, s[2], s[1], s[0]); + s += 4; + } + } + } -void NVGSurface::setRenderThroughImage(bool const shouldRenderThroughImage) -{ - renderThroughImage = shouldRenderThroughImage; - backupImageComponent.setVisible(shouldRenderThroughImage); - if (!shouldRenderThroughImage) - backupRenderImage = Image(); + // Hand back a logical-resolution image so callers can work in logical coords. + if (w != logicalArea.getWidth() || h != logicalArea.getHeight()) + return image.rescaled(logicalArea.getWidth(), logicalArea.getHeight(), Graphics::mediumResamplingQuality); + + return image; } NVGSurface* NVGSurface::getSurfaceForContext(NVGcontext* nvg) @@ -521,7 +731,13 @@ void NVGSurface::removeBufferedObject(NVGComponent* component) bufferedObjects.erase(component); } -void NVGSurface::handleCommandMessage(int commandID) -{ - needsBufferSwap = true; +void NVGSurface::InvalidationListener::paint(Graphics& g) { + if(nvgRepaint) { + if (auto* llgc = dynamic_cast(&g.getInternalContext())) { + if (auto* nvgComponent = dynamic_cast(originComponent)) { + nvgComponent->render(llgc->getContext()); + return; + } + } + } } diff --git a/Source/NVGSurface.h b/Source/NVGSurface.h index b296116adc..3ceca9f050 100644 --- a/Source/NVGSurface.h +++ b/Source/NVGSurface.h @@ -7,42 +7,53 @@ #pragma once #include -#include +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) +# include +#else +# include using namespace juce::gl; +#endif + +#ifndef PLUGDATA_NVG_FRAME_TIME_OVERLAY +# define PLUGDATA_NVG_FRAME_TIME_OVERLAY 0 +#endif + +#ifndef PLUGDATA_NVG_REPAINT_DEBUG +# define PLUGDATA_NVG_REPAINT_DEBUG 0 +#endif #include "Utility/Config.h" #include "Utility/SettingsFile.h" -#include -#ifdef NANOVG_GL_IMPLEMENTATION -# undef NANOVG_GL_IMPLEMENTATION -# include -# define NANOVG_GL_IMPLEMENTATION 1 +#ifdef PLUGDATA_NVG_FRAME_TIME_OVERLAY +#include "Utility/FrameTimeOverlay.h" +#endif + +#include +#include + +#if !NANOVG_METAL_IMPLEMENTATION +#define NANOVG_GL_IMPLEMENTATION 1 #endif -class FrameTimer; class PluginEditor; class NVGComponent; + class NVGSurface final : #if NANOVG_METAL_IMPLEMENTATION && JUCE_MAC - public NSViewComponent + public NSViewComponent, public Thread #elif NANOVG_METAL_IMPLEMENTATION && JUCE_IOS - public UIViewComponent + public UIViewComponent, public Thread #else - public Component + public Component, public OpenGLRenderer #endif + , public AsyncUpdater { public: explicit NVGSurface(PluginEditor* editor); ~NVGSurface() override; void initialise(); - void updateBufferSize(); - - void renderAll(); - void render(); - - void blitToScreen(); bool makeContextActive(); @@ -50,42 +61,71 @@ class NVGSurface final : void lookAndFeelChanged() override; + void handleAsyncUpdate() override; + Rectangle getInvalidArea() const { return invalidArea; } float getRenderScale() const; void updateBounds(Rectangle bounds); + class InvalidationChecker final : public CachedComponentImage { + public: + InvalidationChecker(std::function invalidate) : invalidateCache(invalidate) + { + } + + void paint(Graphics& g) override {}; + + bool invalidate(Rectangle const& rect) override + { + invalidateCache(); + return true; + } + + bool invalidateAll() override + { + invalidateCache(); + return true; + } + + void releaseResources() override {} + + std::function invalidateCache; + }; + class InvalidationListener final : public CachedComponentImage { public: - InvalidationListener(NVGSurface& s, Component* origin, std::function canRepaintCheck = [] { return true; }) + InvalidationListener(NVGSurface& s, Component* origin, bool performNvgRepaint = false, std::function canRepaintCheck = [] { return true; }) : surface(s) , originComponent(origin) , canRepaint(canRepaintCheck) + , nvgRepaint(performNvgRepaint) { } - void paint(Graphics& g) override { } + void paint(Graphics& g) override; bool invalidate(Rectangle const& rect) override { // Translate from canvas coords to viewport coords as float to prevent rounding errors - auto invalidatedBounds = surface.getLocalArea(originComponent, rect.expanded(2).toFloat()).getSmallestIntegerContainer(); + auto invalidatedBounds = surface.getLocalArea(originComponent, rect.toFloat()).getSmallestIntegerContainer(); invalidatedBounds = invalidatedBounds.getIntersection(surface.getLocalBounds()); if (originComponent->isVisible() && canRepaint() && !invalidatedBounds.isEmpty()) { surface.invalidateArea(invalidatedBounds); } - return surface.renderThroughImage; + return false; } bool invalidateAll() override { if (originComponent->isVisible() && canRepaint()) { - surface.invalidateArea(originComponent->getLocalBounds()); + auto invalidatedBounds = surface.getLocalArea(originComponent, originComponent->getLocalBounds()); + surface.invalidateArea(invalidatedBounds); } - return surface.renderThroughImage; + return false; } void releaseResources() override { } @@ -93,79 +133,110 @@ class NVGSurface final : NVGSurface& surface; Component* originComponent; std::function canRepaint; + bool nvgRepaint = false; }; void invalidateArea(Rectangle area); void invalidateAll(); - void setRenderThroughImage(bool renderThroughImage); - static NVGSurface* getSurfaceForContext(NVGcontext*); - void renderFrameToImage(Image& image, Rectangle area); - void resized() override; -#if JUCE_LINUX || JUCE_BSD - bool roundedLeft = false, roundedRight = true; - void setRoundedBottomCorners(bool left, bool right); -#endif - void addBufferedObject(NVGComponent* component); void removeBufferedObject(NVGComponent* component); - void handleCommandMessage(int commandID) override; - - // Sets the surface context to render through floating window, or inside editor as image - void updateWindowContextVisibility(); + // Reads back a region of the rendered framebuffer into a JUCE image. + // 'logicalArea' is in this component's (i.e. the editor's) logical coordinates. + // The returned image is in logical resolution, opaque, top-left origin. + // Used by the eyedropper, which can't snapshot the GPU-rendered UI the normal way. + // Safe to call from the message thread; blocks briefly on the render thread. + Image renderToImage(Rectangle logicalArea); private: + void serviceReadbackRequest(); + float calculateRenderScale() const; + void updateRenderScale(); + void snapshotEditorSize(); + void scheduleRender(); + void recordFrame(); + void createRenderContext(); + void destroyRenderContext(); + void renderBackendFrame(); + void presentFramebuffer(int viewWidth, int viewHeight); + void requestBackendRender(); + +#if PLUGDATA_NVG_FRAME_TIME_OVERLAY + void drawFrameTimeOverlay(int viewWidth, int viewHeight, float scale); +#endif - PluginEditor* editor; - NVGcontext* nvg = nullptr; - bool needsBufferSwap = false; - std::unique_ptr vBlankAttachment; +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + void run() override; +#endif - Rectangle invalidArea; - Rectangle currentBounds; - NVGframebuffer* invalidFBO = nullptr; - int fbWidth = 0, fbHeight = 0; +#if NANOVG_GL_IMPLEMENTATION + void newOpenGLContextCreated() override; + void renderOpenGL() override; + void openGLContextClosing() override; +#endif - static inline UnorderedMap surfaces; + PluginEditor* editor; - juce::Image backupRenderImage; - bool renderThroughImage = false; - bool isRenderingThroughImage = false; + AtomicValue nvg { nullptr }; // async command-recording wrapper (used for all drawing) + NVGcontext* baseNvg = nullptr; // real backend context (framebuffers, blit, teardown) - // To fix rounded corners on Linux/BSD - class ClippedImageComponent : public ImageComponent { -#if JUCE_LINUX || JUCE_BSD - public: - void paint(Graphics& g) - { - g.reduceClipRegion(clipPath); - ImageComponent::paint(g); - } - void setClipPath(Path const& p) { clipPath = p; } + Rectangle invalidArea; // damage accumulated since the last recorded frame (message thread) + Rectangle inFlightDamage; // damage of the last recorded frame, re-folded if it gets coalesced + Rectangle currentBounds; + AtomicValue frameReadyForReplay { false }; + AtomicValue recordedFramebufferWidth { 0 }; + AtomicValue recordedFramebufferHeight { 0 }; + AtomicValue recordedFrameIsFullRepaint { false }; - private: - Path clipPath; -#endif - }; - ClippedImageComponent backupImageComponent; + NVGframebuffer* mainFramebuffer = nullptr; // real backend framebuffer (render thread only) + int mainFramebufferWidth = 0; + int mainFramebufferHeight = 0; + + static inline UnorderedMap surfaces; UnorderedSegmentedSet> bufferedObjects; float lastRenderScale = 0.0f; - uint32 lastRenderTime; -#if JUCE_LINUX - bool skipFrame = false; + + AtomicValue cachedRenderScale { 0.0f }; + + // Editor's logical (pre-scale) size, snapshotted on the message thread. The + // render thread sizes the drawable/framebuffer from these instead of reading + // the editor Component directly: Component bounds are message-thread-only, so + // reading them off the render thread is a data race (see snapshotEditorSize). + AtomicValue editorWidth { 1 }; + AtomicValue editorHeight { 1 }; + + // Framebuffer readback (eyedropper). The request is filled on the message + // thread and serviced on the render thread, which owns the framebuffer. + CriticalSection readbackLock; // serialises readback requests + WaitableEvent readbackReady; // signalled by the render thread + AtomicValue readbackPending { false }; + Rectangle readbackDeviceArea; // requested region, in device pixels + HeapBlock readbackData; // filled with BGRA pixels + int readbackWidth = 0; + int readbackHeight = 0; + bool readbackSucceeded = false; + +#if PLUGDATA_NVG_FRAME_TIME_OVERLAY + std::unique_ptr frameTimeOverlay; + std::unique_ptr frameTimeVBlankAttachment; #endif #if NANOVG_GL_IMPLEMENTATION std::unique_ptr glContext; #endif - std::unique_ptr frameTimer; +#if NANOVG_METAL_IMPLEMENTATION && (JUCE_MAC || JUCE_IOS) + void* metalView = nullptr; + void* metalLayer = nullptr; // the view's CAMetalLayer, cached on the message + // thread so the render thread never touches the view + AtomicValue backendRenderRequested { false }; +#endif }; diff --git a/Source/Object.cpp b/Source/Object.cpp index 4eac04ae6d..d817c337fd 100644 --- a/Source/Object.cpp +++ b/Source/Object.cpp @@ -96,6 +96,10 @@ void Object::setObjectBounds(Rectangle const bounds) void Object::initialise() { + setCachedComponentImage(new NVGSurface::InvalidationChecker([this](){ + commandBufferDirty = true; + })); + cnv->objectLayer.addAndMakeVisible(this); cnv->selectedComponents.addChangeListener(this); @@ -457,12 +461,12 @@ void Object::setType(String const& newType, pd::WeakReference existingObject) cnv->pd->updateObjectImplementations(); } -SmallArray> Object::getCorners() const +StackArray, 4> Object::getCorners() const { auto const rect = getLocalBounds().reduced(margin); constexpr float offset = 2.0f; - SmallArray> corners = { Rectangle(9.0f, 9.0f).withCentre(rect.getTopLeft().toFloat()).translated(offset, offset), Rectangle(9.0f, 9.0f).withCentre(rect.getBottomLeft().toFloat()).translated(offset, -offset), + StackArray, 4> corners = { Rectangle(9.0f, 9.0f).withCentre(rect.getTopLeft().toFloat()).translated(offset, offset), Rectangle(9.0f, 9.0f).withCentre(rect.getBottomLeft().toFloat()).translated(offset, -offset), Rectangle(9.0f, 9.0f).withCentre(rect.getBottomRight().toFloat()).translated(-offset, -offset), Rectangle(9.0f, 9.0f).withCentre(rect.getTopRight().toFloat()).translated(-offset, offset) }; return corners; @@ -1172,6 +1176,18 @@ void Object::mouseDrag(MouseEvent const& e) } void Object::render(NVGcontext* nvg) +{ + if(commandBufferDirty) { + commandBuffer.clear(); + nanovg::ScopedCommandRecorder recorder(nvg, commandBuffer); + performRender(nvg); + commandBufferDirty = false; + } + + nanovg::replay(nvg, commandBuffer); +} + +void Object::performRender(NVGcontext* nvg) { auto const lb = getLocalBounds(); auto const b = lb.reduced(margin); @@ -1179,72 +1195,67 @@ void Object::render(NVGcontext* nvg) if (cnv->shouldShowObjectActivity() && !approximatelyEqual(activeStateAlpha, 0.0f)) { auto glowColour = nvgColour(PlugDataColours::dataColour); glowColour.a = static_cast(activeStateAlpha * 255); - nvgSmoothGlow(nvg, lb.getX(), lb.getY(), lb.getWidth(), lb.getHeight(), glowColour, nvgRGBA(0, 0, 0, 0), Corners::objectCornerRadius, 1.1f); + nanovg::nvgSmoothGlow(nvg, lb.getX(), lb.getY(), lb.getWidth(), lb.getHeight(), glowColour, nanovg::nvgRGBA(0, 0, 0, 0), Corners::objectCornerRadius, 1.1f); } if (selectedFlag && showHandles) { - auto& resizeHandleImage = cnv->resizeHandleImage; int angle = 360; for (auto& corner : getCorners()) { NVGScopedState scopedState(nvg); // Rotate around centre - nvgTranslate(nvg, corner.getCentreX(), corner.getCentreY()); - nvgRotate(nvg, degreesToRadians(angle)); - nvgTranslate(nvg, -4.5f, -4.5f); - - nvgBeginPath(nvg); - nvgRect(nvg, 0, 0, 9, 9); - nvgFillPaint(nvg, nvgImageAlphaPattern(nvg, 0, 0, 9, 9, 0, resizeHandleImage.getImageId(), nvgColour(PlugDataColours::objectSelectedOutlineColour))); - nvgFill(nvg); + nanovg::nvgTranslate(nvg, corner.getCentreX(), corner.getCentreY()); + nanovg::nvgRotate(nvg, degreesToRadians(angle)); + nanovg::nvgTranslate(nvg, -4.5f, -4.5f); + + cnv->renderResizeHandle(nvg, nvgColour(PlugDataColours::objectSelectedOutlineColour)); angle -= 90; } } if (gui && gui->isTransparent() && !getValue(locked) && !cnv->isGraph) { - nvgFillColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour.contrasting(0.35f).withAlpha(0.1f))); - nvgFillRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius); + nanovg::nvgFillColor(nvg, nvgColour(PlugDataColours::canvasBackgroundColour.contrasting(0.35f).withAlpha(0.1f))); + nanovg::nvgFillRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius); } - nvgTranslate(nvg, margin, margin); + nanovg::nvgTranslate(nvg, margin, margin); if (gui) { gui->render(nvg); } if (newObjectEditor) { - nvgDrawRoundedRect(nvg, 0, 0, b.getWidth(), b.getHeight(), nvgColour(PlugDataColours::textObjectBackgroundColour), nvgColour(isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); - Graphics g(*editor->getNanoLLGC()); - newObjectEditor->paintEntireComponent(g, true); + nanovg::nvgDrawRoundedRect(nvg, 0, 0, b.getWidth(), b.getHeight(), nvgColour(PlugDataColours::textObjectBackgroundColour), nvgColour(isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); + editor->getNanoLLGC()->renderComponent(*newObjectEditor); } // If autoconnect is about to happen, draw a fake inlet with a dotted outline if (isInitialEditorShown() && cnv->lastSelectedObject && cnv->lastSelectedObject != this && cnv->lastSelectedObject->numOutputs && getValue(editor->autoconnect)) { auto const* outlet = cnv->lastSelectedObject->iolets[cnv->lastSelectedObject->numInputs]; SmallArray fakeInletBounds = PlugDataLook::getUseIoletSpacingEdge() ? SmallArray { -8.0f, -3.0f, 18.0f, 7.0f } : SmallArray { 8.5f, -3.5f, 8.0f, 8.0f }; - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); if (PlugDataLook::getUseSquareIolets()) { - nvgRect(nvg, fakeInletBounds[0] + fakeInletBounds[2] * 0.5f, fakeInletBounds[1] + fakeInletBounds[3] * 0.5f, fakeInletBounds[2] * 0.5f, fakeInletBounds[3] * 0.5f); + nanovg::nvgRect(nvg, fakeInletBounds[0] + fakeInletBounds[2] * 0.5f, fakeInletBounds[1] + fakeInletBounds[3] * 0.5f, fakeInletBounds[2] * 0.5f, fakeInletBounds[3] * 0.5f); } else { - nvgEllipse(nvg, fakeInletBounds[0] + fakeInletBounds[2] * 0.5f, fakeInletBounds[1] + fakeInletBounds[3] * 0.5f, fakeInletBounds[2] * 0.5f, fakeInletBounds[3] * 0.5f); + nanovg::nvgEllipse(nvg, fakeInletBounds[0] + fakeInletBounds[2] * 0.5f, fakeInletBounds[1] + fakeInletBounds[3] * 0.5f, fakeInletBounds[2] * 0.5f, fakeInletBounds[3] * 0.5f); } - nvgFillColor(nvg, nvgColour(outlet->isSignal() ? PlugDataColours::signalColour.brighter() : PlugDataColours::dataColour.brighter())); - nvgFill(nvg); + nanovg::nvgFillColor(nvg, nvgColour(outlet->isSignal() ? PlugDataColours::signalColour.brighter() : PlugDataColours::dataColour.brighter())); + nanovg::nvgFill(nvg); - nvgStrokeColor(nvg, nvgColour(PlugDataColours::objectOutlineColour)); - nvgStrokeWidth(nvg, 1.0f); - nvgStroke(nvg); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::objectOutlineColour)); + nanovg::nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgStroke(nvg); } - nvgTranslate(nvg, -margin, -margin); + nanovg::nvgTranslate(nvg, -margin, -margin); if (!isHvccCompatible) { NVGScopedState scopedState(nvg); - nvgBeginPath(nvg); - nvgStrokeColor(nvg, nvgRGBA(255, 127, 0.0f, 255)); // orange - nvgStrokeWidth(nvg, 1.0f); - nvgRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgStrokeColor(nvg, nanovg::nvgRGBA(255, 127, 0.0f, 255)); // orange + nanovg::nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius); + nanovg::nvgStroke(nvg); } else if (cnv->shouldShowIndex()) { constexpr int halfHeight = 5; @@ -1253,13 +1264,9 @@ void Object::render(NVGcontext* nvg) auto const indexBounds = b.withSizeKeepingCentre(b.getWidth() + doubleMargin, halfHeight * 2).removeFromRight(textWidth); auto const fillColour = nvgColour(PlugDataColours::objectSelectedOutlineColour); - nvgDrawRoundedRect(nvg, indexBounds.getX(), indexBounds.getY(), indexBounds.getWidth(), indexBounds.getHeight(), fillColour, fillColour, 2.0f); + nanovg::nvgDrawRoundedRect(nvg, indexBounds.getX(), indexBounds.getY(), indexBounds.getWidth(), indexBounds.getHeight(), fillColour, fillColour, 2.0f); - nvgFontSize(nvg, 8.0f); - nvgFontFace(nvg, "Inter-Regular"); - nvgTextAlign(nvg, NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER); - nvgFillColor(nvg, nvgColour(PlugDataColours::objectSelectedOutlineColour.contrasting())); - nvgText(nvg, indexBounds.getCentreX(), indexBounds.getCentreY(), text.c_str(), nullptr); + Fonts::drawText(cnv->editor->getNanoLLGC(), text, indexBounds.toFloat(), Fonts::getCurrentFont().withHeight(8.0f), PlugDataColours::objectSelectedOutlineColour.contrasting(), Justification::centred); } renderIolets(nvg); @@ -1272,26 +1279,26 @@ void Object::renderIolets(NVGcontext* nvg) if (getValue(locked) || !drawIoletExpanded) { auto const clipBounds = getLocalBounds().reduced(Object::margin); - nvgIntersectScissor(nvg, clipBounds.getX(), clipBounds.getY(), clipBounds.getWidth(), clipBounds.getHeight()); + nanovg::nvgIntersectScissor(nvg, clipBounds.getX(), clipBounds.getY(), clipBounds.getWidth(), clipBounds.getHeight()); } else if (getValue(patchDownwardsOnly)) { auto const clipBounds = getLocalBounds().reduced(Object::margin); - nvgIntersectScissor(nvg, clipBounds.getX(), clipBounds.getY(), clipBounds.getWidth(), clipBounds.getHeight() + Object::doubleMargin); + nanovg::nvgIntersectScissor(nvg, clipBounds.getX(), clipBounds.getY(), clipBounds.getWidth(), clipBounds.getHeight() + Object::doubleMargin); } auto lastPosition = Point(); for (auto* iolet : iolets) { - nvgTranslate(nvg, iolet->getX() - lastPosition.x, iolet->getY() - lastPosition.y); + nanovg::nvgTranslate(nvg, iolet->getX() - lastPosition.x, iolet->getY() - lastPosition.y); if (iolet->isTargeted()) { - nvgSave(nvg); - nvgResetScissor(nvg); + nanovg::nvgSave(nvg); + nanovg::nvgResetScissor(nvg); } iolet->render(nvg); lastPosition = iolet->getPosition(); if (iolet->isTargeted()) { - nvgRestore(nvg); + nanovg::nvgRestore(nvg); } } } @@ -1299,11 +1306,12 @@ void Object::renderIolets(NVGcontext* nvg) void Object::renderLabel(NVGcontext* nvg) { if (gui) { + auto& llgc = *cnv->editor->getNanoLLGC(); for (auto* label : gui->labels) { NVGScopedState scopedState(nvg); - nvgTranslate(nvg, label->getX(), label->getY()); + nanovg::nvgTranslate(nvg, label->getX(), label->getY()); if (label->isVisible()) { - label->renderLabel(nvg, gui->getImageScale()); + label->render(llgc); } } } diff --git a/Source/Object.h b/Source/Object.h index 3b146631e1..934217d8e5 100644 --- a/Source/Object.h +++ b/Source/Object.h @@ -15,13 +15,10 @@ #include "Pd/WeakReference.h" #include "Iolet.h" -#include +#include #ifdef NANOVG_GL_IMPLEMENTATION # include using namespace juce::gl; -# undef NANOVG_GL_IMPLEMENTATION -# include -# define NANOVG_GL_IMPLEMENTATION 1 #endif struct ObjectDragState; @@ -83,7 +80,7 @@ class Object final : public Component void mouseExit(MouseEvent const& e) override; void render(NVGcontext* nvg) override; - + void performRender(NVGcontext* nvg); void renderIolets(NVGcontext* nvg); void renderLabel(NVGcontext* nvg); @@ -101,7 +98,7 @@ class Object final : public Component void triggerOverlayActiveState(); - SmallArray> getCorners() const; + StackArray, 4> getCorners() const; uint16_t numInputs = 0; uint16_t numOutputs = 0; @@ -162,6 +159,10 @@ class Object final : public Component bool isHvccCompatible : 1 = true; bool isGemObject : 1 = false; bool isObjectMouseActive : 1 = false; + + bool commandBufferDirty : 1 = true; + nanovg::CommandBuffer commandBuffer; + ObjectDragState& ds; diff --git a/Source/ObjectGrid.cpp b/Source/ObjectGrid.cpp index 6c3301b10d..815698fc37 100644 --- a/Source/ObjectGrid.cpp +++ b/Source/ObjectGrid.cpp @@ -82,7 +82,10 @@ void ObjectGrid::startLineFadeAnimation(int idx, float ms, float targetAlpha) .withValueChangedCallback([this, idx](float v) { lineAlpha[idx] = makeAnimationLimits(lineAlpha[idx], lineTargetAlpha[idx]).lerp(v); auto const lineArea = cnv->editor->nvgSurface.getLocalArea(cnv, Rectangle(lines[idx].getStart(), lines[idx].getEnd()).expanded(2)); - cnv->editor->nvgSurface.invalidateArea(lineArea); + + if(cnv->editor->nvgSurface.getLocalBounds().contains(lineArea)) { + cnv->editor->nvgSurface.invalidateArea(lineArea); + } }) .build(); updater.addAnimator(lineAnimators[idx], [this, idx]() { @@ -455,22 +458,22 @@ void ObjectGrid::setIndicator(int const idx, Line const line) void ObjectGrid::render(NVGcontext* nvg) { if (lines[0].getLength() != 0) { - nvgStrokeColor(nvg, nvgColour(PlugDataColours::gridLineColour.withAlpha(lineAlpha[0]))); - nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::gridLineColour.withAlpha(lineAlpha[0]))); + nanovg::nvgStrokeWidth(nvg, 1.0f); - nvgBeginPath(nvg); - nvgMoveTo(nvg, lines[0].getStartX(), lines[0].getStartY()); - nvgLineTo(nvg, lines[0].getEndX(), lines[0].getEndY()); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, lines[0].getStartX(), lines[0].getStartY()); + nanovg::nvgLineTo(nvg, lines[0].getEndX(), lines[0].getEndY()); + nanovg::nvgStroke(nvg); } if (lines[1].getLength() != 0) { - nvgStrokeColor(nvg, nvgColour(PlugDataColours::gridLineColour.withAlpha(lineAlpha[1]))); - nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::gridLineColour.withAlpha(lineAlpha[1]))); + nanovg::nvgStrokeWidth(nvg, 1.0f); - nvgBeginPath(nvg); - nvgMoveTo(nvg, lines[1].getStartX(), lines[1].getStartY()); - nvgLineTo(nvg, lines[1].getEndX(), lines[1].getEndY()); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, lines[1].getStartX(), lines[1].getStartY()); + nanovg::nvgLineTo(nvg, lines[1].getEndX(), lines[1].getEndY()); + nanovg::nvgStroke(nvg); } } diff --git a/Source/Objects/ArrayObject.h b/Source/Objects/ArrayObject.h index 9b3d6b9c66..9d307a9250 100644 --- a/Source/Objects/ArrayObject.h +++ b/Source/Objects/ArrayObject.h @@ -229,16 +229,16 @@ class GraphicalArray final : public Component } NVGScopedState scopedState(nvg); auto const arrB = getLocalBounds().reduced(1); - nvgIntersectRoundedScissor(nvg, arrB.getX(), arrB.getY(), arrB.getWidth(), arrB.getHeight(), Corners::objectCornerRadius); + nanovg::nvgIntersectRoundedScissor(nvg, arrB.getX(), arrB.getY(), arrB.getWidth(), arrB.getHeight(), Corners::objectCornerRadius); if (cachedPath.isValid()) { auto const contentColour = getContentColour(); if (arrDrawMode == Points) { - nvgFillColor(nvg, nvgRGBA(contentColour.getRed(), contentColour.getGreen(), contentColour.getBlue(), contentColour.getAlpha())); + nanovg::nvgFillColor(nvg, nanovg::nvgRGBA(contentColour.getRed(), contentColour.getGreen(), contentColour.getBlue(), contentColour.getAlpha())); cachedPath.fill(); } else { - nvgStrokeColor(nvg, nvgRGBA(contentColour.getRed(), contentColour.getGreen(), contentColour.getBlue(), contentColour.getAlpha())); - nvgStrokeWidth(nvg, getLineWidth()); + nanovg::nvgStrokeColor(nvg, nanovg::nvgRGBA(contentColour.getRed(), contentColour.getGreen(), contentColour.getBlue(), contentColour.getAlpha())); + nanovg::nvgStrokeWidth(nvg, getLineWidth()); cachedPath.stroke(); } return; @@ -249,13 +249,13 @@ class GraphicalArray final : public Component auto const contentColour = getContentColour(); if (arrDrawMode == Points) { - nvgFillColor(nvg, nvgRGBA(contentColour.getRed(), contentColour.getGreen(), contentColour.getBlue(), contentColour.getAlpha())); - nvgFill(nvg); + nanovg::nvgFillColor(nvg, nanovg::nvgRGBA(contentColour.getRed(), contentColour.getGreen(), contentColour.getBlue(), contentColour.getAlpha())); + nanovg::nvgFill(nvg); cachedPath.save(nvg); } else { - nvgStrokeColor(nvg, nvgRGBA(contentColour.getRed(), contentColour.getGreen(), contentColour.getBlue(), contentColour.getAlpha())); - nvgStrokeWidth(nvg, getLineWidth()); - nvgStroke(nvg); + nanovg::nvgStrokeColor(nvg, nanovg::nvgRGBA(contentColour.getRed(), contentColour.getGreen(), contentColour.getBlue(), contentColour.getAlpha())); + nanovg::nvgStrokeWidth(nvg, getLineWidth()); + nanovg::nvgStroke(nvg); cachedPath.save(nvg); } } @@ -325,18 +325,15 @@ class GraphicalArray final : public Component if (error) { auto const position = getLocalBounds().getCentre(); auto const errorText = "array " + getUnexpandedName() + " is invalid"; - nvgFontSize(nvg, 11); - nvgFontFace(nvg, "Inter-Regular"); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgFillColor(nvg, nvgColour(PlugDataColours::canvasTextColour)); - nvgText(nvg, position.x, position.y, errorText.toRawUTF8(), nullptr); + + Fonts::drawText(object->cnv->editor->getNanoLLGC(), errorText, position.toFloat(), Fonts::getCurrentFont().withHeight(11), PlugDataColours::canvasTextColour, Justification::centred); error = false; } else if (visible) { paintGraph(nvg); } if (isDraggingFile) { - nvgDrawRoundedRect(nvg, 0, 0, getWidth(), getHeight(), nvgRGBA(0, 0, 0, 0), nvgColour(PlugDataColours::dataColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, 0, 0, getWidth(), getHeight(), nanovg::nvgRGBA(0, 0, 0, 0), nvgColour(PlugDataColours::dataColour), Corners::objectCornerRadius); } } @@ -1052,7 +1049,7 @@ class ArrayEditorDialog final : public Component { resizer.setAllowHostManagedResize(false); // Position in centre of screen - setBounds(Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea.withSizeKeepingCentre(600, 400)); + setBounds(Desktop::getInstance().getDisplays().getPrimaryDisplay()->userBounds.getSmallestIntegerContainer().withSizeKeepingCentre(600, 400)); addAndMakeVisible(resizer); updateGraphs(); @@ -1246,17 +1243,17 @@ class ArrayObject final : public ObjectBase { void render(NVGcontext* nvg) override { auto const b = getLocalBounds().toFloat(); - auto const backgroundColour = nvgRGBA(0, 0, 0, 0); + auto const backgroundColour = nanovg::nvgRGBA(0, 0, 0, 0); auto const selectedOutlineColour = nvgColour(PlugDataColours::objectSelectedOutlineColour); auto const outlineColour = nvgColour(PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, object->isSelected() ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, object->isSelected() ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); for (auto* graph : graphs) { graph->render(nvg); } - nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); getTicks()->render(nvg, b); } diff --git a/Source/Objects/BangObject.h b/Source/Objects/BangObject.h index 5aac58d400..3a4f3655e4 100644 --- a/Source/Objects/BangObject.h +++ b/Source/Objects/BangObject.h @@ -121,7 +121,7 @@ class BangObject final : public ObjectBase { auto b = getLocalBounds().toFloat(); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), bgCol, object->isSelected() ? nvgColour(PlugDataColours::objectSelectedOutlineColour) : nvgColour(PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), bgCol, object->isSelected() ? nvgColour(PlugDataColours::objectSelectedOutlineColour) : nvgColour(PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); b = b.reduced(1); auto const width = std::max(b.getWidth(), b.getHeight()); @@ -133,16 +133,16 @@ class BangObject final : public ObjectBase auto const outerCircleBounds = b.reduced((width - circleOuter) * sizeReduction); - nvgBeginPath(nvg); - nvgCircle(nvg, b.getCentreX(), b.getCentreY(), outerCircleBounds.getWidth() / 2.0f); - nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); - nvgStrokeWidth(nvg, circleThickness); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgCircle(nvg, b.getCentreX(), b.getCentreY(), outerCircleBounds.getWidth() / 2.0f); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); + nanovg::nvgStrokeWidth(nvg, circleThickness); + nanovg::nvgStroke(nvg); // Fill ellipse if bangState is true if (bangState) { auto const iCB = b.reduced((width - circleOuter + circleThickness) * sizeReduction); - nvgDrawRoundedRect(nvg, iCB.getX(), iCB.getY(), iCB.getWidth(), iCB.getHeight(), fgCol, fgCol, iCB.getWidth() * 0.5f); + nanovg::nvgDrawRoundedRect(nvg, iCB.getX(), iCB.getY(), iCB.getWidth(), iCB.getHeight(), fgCol, fgCol, iCB.getWidth() * 0.5f); } } diff --git a/Source/Objects/BicoeffObject.h b/Source/Objects/BicoeffObject.h index dc955b9330..851833e8f9 100644 --- a/Source/Objects/BicoeffObject.h +++ b/Source/Objects/BicoeffObject.h @@ -258,29 +258,29 @@ class BicoeffGraph final : public Component auto const selectedOutlineColour = nvgColour(PlugDataColours::objectSelectedOutlineColour); auto const outlineColour = nvgColour(PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, object->isSelected() ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); - - nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); - nvgBeginPath(nvg); - nvgMoveTo(nvg, filterX1 * getWidth(), 0.0f); - nvgLineTo(nvg, filterX1 * getWidth(), getHeight()); - nvgStroke(nvg); - - nvgBeginPath(nvg); - nvgMoveTo(nvg, filterX2 * getWidth(), 0.0f); - nvgLineTo(nvg, filterX2 * getWidth(), getHeight()); - nvgStroke(nvg); - - nvgBeginPath(nvg); - nvgMoveTo(nvg, 0.0f, getHeight() / 2.0f); - nvgLineTo(nvg, getWidth(), getHeight() / 2.0f); - nvgStroke(nvg); - - nvgStrokeWidth(nvg, 1.0f); - nvgLineStyle(nvg, NVG_BUTT); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, object->isSelected() ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); + + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, filterX1 * getWidth(), 0.0f); + nanovg::nvgLineTo(nvg, filterX1 * getWidth(), getHeight()); + nanovg::nvgStroke(nvg); + + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, filterX2 * getWidth(), 0.0f); + nanovg::nvgLineTo(nvg, filterX2 * getWidth(), getHeight()); + nanovg::nvgStroke(nvg); + + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, 0.0f, getHeight() / 2.0f); + nanovg::nvgLineTo(nvg, getWidth(), getHeight() / 2.0f); + nanovg::nvgStroke(nvg); + + nanovg::nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgLineStyle(nvg, NVG_BUTT); setJUCEPath(nvg, magnitudePath); - nvgStrokeColor(nvg, nvgColour(PlugDataColours::canvasTextColour)); - nvgStroke(nvg); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::canvasTextColour)); + nanovg::nvgStroke(nvg); } void changeBandWidth(float const x, float const y, float const previousX, float const previousY) diff --git a/Source/Objects/ButtonObject.h b/Source/Objects/ButtonObject.h index f8bc57cd02..e59b9be80b 100644 --- a/Source/Objects/ButtonObject.h +++ b/Source/Objects/ButtonObject.h @@ -51,16 +51,16 @@ class ButtonObject final : public ObjectBase { void updateColours() { - bgCol = nvgColour(Colour::fromString(secondaryColour.toString())); - fgCol = nvgColour(Colour::fromString(primaryColour.toString())); + bgCol = nvgColour(getValue(secondaryColour)); + fgCol = nvgColour(getValue(primaryColour)); repaint(); } void update() override { if (auto button = ptr.get()) { - primaryColour = String::fromUTF8(button->x_fg->s_name).replace("#", "ff"); - secondaryColour = String::fromUTF8(button->x_bg->s_name).replace("#", "ff"); + primaryColour = colourToVar(Colour::fromString(String::fromUTF8(button->x_fg->s_name).replace("#", "ff"))); + secondaryColour = colourToVar(Colour::fromString(String::fromUTF8(button->x_bg->s_name).replace("#", "ff"))); transparent = button->x_transparent; sizeProperty = VarArray(button->x_w, button->x_h); readOnly = button->x_readonly; @@ -270,8 +270,8 @@ class ButtonObject final : public ObjectBase { { auto const b = getLocalBounds().toFloat(); - auto fillColour = getValue(transparent) ? nvgRGBA(0, 0, 0, 0) : bgCol; - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), fillColour, object->isSelected() ? nvgColour(PlugDataColours::objectSelectedOutlineColour) : nvgColour(PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); + auto fillColour = getValue(transparent) ? nanovg::nvgRGBA(0, 0, 0, 0) : bgCol; + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), fillColour, object->isSelected() ? nvgColour(PlugDataColours::objectSelectedOutlineColour) : nvgColour(PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); auto radius = getValue(oval) ? getWidth() : Corners::objectCornerRadius; if (!getValue(transparent)) { @@ -282,15 +282,15 @@ class ButtonObject final : public ObjectBase { if (b.getWidth() >= 25 && b.getHeight() >= 25) { spaceToShowRect = true; - nvgDrawRoundedRect(nvg, outerBounds.getX(), outerBounds.getY(), outerBounds.getWidth(), outerBounds.getHeight(), nvgColour(PlugDataColours::guiObjectInternalOutlineColour), nvgColour(PlugDataColours::guiObjectInternalOutlineColour), radius); - nvgDrawRoundedRect(nvg, innerRectBounds.getX(), innerRectBounds.getY(), innerRectBounds.getWidth(), innerRectBounds.getHeight(), bgCol, bgCol, radius - 1.0f); + nanovg::nvgDrawRoundedRect(nvg, outerBounds.getX(), outerBounds.getY(), outerBounds.getWidth(), outerBounds.getHeight(), nvgColour(PlugDataColours::guiObjectInternalOutlineColour), nvgColour(PlugDataColours::guiObjectInternalOutlineColour), radius); + nanovg::nvgDrawRoundedRect(nvg, innerRectBounds.getX(), innerRectBounds.getY(), innerRectBounds.getWidth(), innerRectBounds.getHeight(), bgCol, bgCol, radius - 1.0f); } // Fill ellipse if bangState is true if (state) { auto const innerBounds = spaceToShowRect ? innerRectBounds.reduced(1) : guiBounds; auto const cornerRadius = spaceToShowRect ? radius - 1.5f : radius - 1; - nvgDrawRoundedRect(nvg, innerBounds.getX(), innerBounds.getY(), innerBounds.getWidth(), innerBounds.getHeight(), fgCol, fgCol, cornerRadius); + nanovg::nvgDrawRoundedRect(nvg, innerBounds.getX(), innerBounds.getY(), innerBounds.getWidth(), innerBounds.getHeight(), fgCol, fgCol, cornerRadius); } } } @@ -317,12 +317,12 @@ class ButtonObject final : public ObjectBase { object->updateBounds(); } else if (value.refersToSameSourceAs(primaryColour)) { if (auto button = ptr.get()) { - button->x_fg = pd->generateSymbol("#" + primaryColour.toString().substring(2)); + button->x_fg = pd->generateSymbol("#" + getValue(primaryColour).toString().substring(2)); } updateColours(); } else if (value.refersToSameSourceAs(secondaryColour)) { if (auto button = ptr.get()) { - button->x_bg = pd->generateSymbol("#" + secondaryColour.toString().substring(2)); + button->x_bg = pd->generateSymbol("#" + getValue(secondaryColour).toString().substring(2)); } updateColours(); } @@ -345,14 +345,14 @@ class ButtonObject final : public ObjectBase { switch (symbol) { case hash("bgcolor"): { if (atoms.size() >= 3) { - setParameterExcludingListener(secondaryColour, Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()).toString()); + setParameterExcludingListener(secondaryColour, colourToVar(Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()))); updateColours(); } break; } case hash("fgcolor"): { if (atoms.size() >= 3) { - setParameterExcludingListener(primaryColour, Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()).toString()); + setParameterExcludingListener(primaryColour, colourToVar(Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()))); updateColours(); } break; diff --git a/Source/Objects/CanvasObject.h b/Source/Objects/CanvasObject.h index 139f0ea15f..717b31dee9 100644 --- a/Source/Objects/CanvasObject.h +++ b/Source/Objects/CanvasObject.h @@ -25,7 +25,7 @@ class CanvasObject final : public ObjectBase { object->setColour(PlugDataColour::outlineColourId, Colours::transparentBlack); iemHelper.iemColourChangedCallback = [this] { - bgColour = Colour::fromString(iemHelper.secondaryColour.toString()); + bgColour = getValue(iemHelper.secondaryColour); bgCol = nvgColour(bgColour); selectionAreaCol = nvgColour(bgColour.contrasting(0.75f)); }; @@ -173,11 +173,11 @@ class CanvasObject final : public ObjectBase { void render(NVGcontext* nvg) override { auto const b = getLocalBounds().toFloat(); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), bgCol, bgCol, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), bgCol, bgCol, Corners::objectCornerRadius); if (!cnv->isGraph && !getValue(object->locked) && !getValue(object->commandLocked) && !hideHitArea) { auto const selectionRectColour = object->isSelected() || isMouseOver() ? nvgColour(PlugDataColours::objectSelectedOutlineColour) : selectionAreaCol; - nvgDrawRoundedRect(nvg, hitArea.getX(), hitArea.getY(), hitArea.getWidth(), hitArea.getHeight(), nvgRGBA(0, 0, 0, 0), selectionRectColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, hitArea.getX(), hitArea.getY(), hitArea.getWidth(), hitArea.getHeight(), nanovg::nvgRGBA(0, 0, 0, 0), selectionRectColour, Corners::objectCornerRadius); } } diff --git a/Source/Objects/CommentObject.h b/Source/Objects/CommentObject.h index 4c37a9acd6..7371405226 100644 --- a/Source/Objects/CommentObject.h +++ b/Source/Objects/CommentObject.h @@ -17,7 +17,7 @@ class CommentObject final : public ObjectBase BorderSize border = BorderSize(1, 0, 1, 0); String objectText; - CachedTextRender textRenderer; + TextLayout layout; public: CommentObject(pd::WeakReference obj, Object* object) @@ -50,11 +50,14 @@ class CommentObject final : public ObjectBase { auto const bounds = getLocalBounds(); + auto& llgc = *cnv->editor->getNanoLLGC(); if (editor) { - Graphics g(*cnv->editor->getNanoLLGC()); - editor->paintEntireComponent(g, true); + llgc.renderComponent(*editor); } else { - textRenderer.renderText(nvg, border.subtractedFrom(bounds).toFloat(), getImageScale()); + Graphics g(llgc); + auto const textBounds = border.subtractedFrom(bounds).toFloat(); + NVGGraphicsContext::ScopedAnchoredDraw anchor(llgc, textBounds); + layout.draw(g, textBounds); } } @@ -214,8 +217,6 @@ class CommentObject final : public ObjectBase fontWidth = glist_fontwidth(cnv->patch.getRawPointer()); } - auto const textSize = textRenderer.getTextBounds(); - int const idealWidth = CachedFontStringWidth::get()->calculateStringWidth(Fonts::getCurrentFont().withHeight(14.5f).withHorizontalScale(calculateHorizontalScale()), objText) + 2; // We want to adjust the width so ideal text with aligns with fontWidth @@ -230,7 +231,7 @@ class CommentObject final : public ObjectBase textWidth = std::max(charWidth, 2) * fontWidth + offset; } - return { textWidth, textSize.getHeight() }; + return { textWidth, static_cast(layout.getHeight()) }; } void lookAndFeelChanged() override @@ -244,10 +245,14 @@ class CommentObject final : public ObjectBase auto const colour = PlugDataColours::commentTextColour; int const textWidth = getTextSize().getWidth(); - int const size = getValue(sizeProperty); - if (textRenderer.prepareLayout(objText, Fonts::getCurrentFont().withHeight(14.5f).withHorizontalScale(calculateHorizontalScale()), colour, textWidth, size ? getValue(sizeProperty) : getWidth(), false)) { - repaint(); - } + + AttributedString attributedText(objText); + attributedText.setColour(colour); + attributedText.setFont(Fonts::getCurrentFont().withHeight(14.5f).withHorizontalScale(calculateHorizontalScale())); + attributedText.setJustification(Justification::centredLeft); + attributedText.setWordWrap(AttributedString::byChar); + layout.createLayout(attributedText, textWidth); + repaint(); } std::unique_ptr createConstrainer() override diff --git a/Source/Objects/DropzoneObject.h b/Source/Objects/DropzoneObject.h index bc66b9c1f8..d18840455f 100644 --- a/Source/Objects/DropzoneObject.h +++ b/Source/Objects/DropzoneObject.h @@ -26,15 +26,15 @@ class DropzoneObject final : public ObjectBase auto const b = getLocalBounds().toFloat(); auto const fillColour = nvgColour(Colours::transparentBlack); auto const outlineColour = nvgColour(object->isSelected() && !cnv->isGraph ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), fillColour, outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), fillColour, outlineColour, Corners::objectCornerRadius); if (isDraggingOver) { auto const hoverBounds = getLocalBounds().reduced(1.5f).toFloat(); - nvgBeginPath(nvg); - nvgRoundedRect(nvg, hoverBounds.getX(), hoverBounds.getY(), hoverBounds.getWidth(), hoverBounds.getHeight(), Corners::objectCornerRadius); - nvgStrokeWidth(nvg, 3.0f); - nvgStrokeColor(nvg, outlineColour); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRoundedRect(nvg, hoverBounds.getX(), hoverBounds.getY(), hoverBounds.getWidth(), hoverBounds.getHeight(), Corners::objectCornerRadius); + nanovg::nvgStrokeWidth(nvg, 3.0f); + nanovg::nvgStrokeColor(nvg, outlineColour); + nanovg::nvgStroke(nvg); } } diff --git a/Source/Objects/FloatAtomObject.h b/Source/Objects/FloatAtomObject.h index e04116dc45..3aa987ddd5 100644 --- a/Source/Objects/FloatAtomObject.h +++ b/Source/Objects/FloatAtomObject.h @@ -157,7 +157,7 @@ class FloatAtomObject final : public ObjectBase { auto const sb = b.reduced(0.5f); // Draw background - nvgDrawObjectWithFlag(nvg, sb.getX(), sb.getY(), sb.getWidth(), sb.getHeight(), + nanovg::nvgDrawObjectWithFlag(nvg, sb.getX(), sb.getY(), sb.getWidth(), sb.getHeight(), nvgColour(PlugDataColours::guiObjectBackgroundColour), nvgColour(PlugDataColours::guiObjectBackgroundColour), nvgColour(PlugDataColours::guiObjectBackgroundColour), Corners::objectCornerRadius, ObjectFlagType::FlagTop, PlugDataLook::getUseFlagOutline()); @@ -169,8 +169,8 @@ class FloatAtomObject final : public ObjectBase { auto const outlineCol = nvgColour((object->isSelected() || hasKeyboardFocus(true)) ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); // Fill the internal of the shape with transparent colour, draw outline & flag with shader - nvgDrawObjectWithFlag(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), - nvgRGBA(0, 0, 0, 0), outlineCol, flagCol, + nanovg::nvgDrawObjectWithFlag(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), + nanovg::nvgRGBA(0, 0, 0, 0), outlineCol, flagCol, Corners::objectCornerRadius, ObjectFlagType::FlagTop, PlugDataLook::getUseFlagOutline()); } diff --git a/Source/Objects/FunctionObject.h b/Source/Objects/FunctionObject.h index cb511df889..4c17cec255 100644 --- a/Source/Objects/FunctionObject.h +++ b/Source/Objects/FunctionObject.h @@ -37,8 +37,8 @@ class FunctionObject final : public ObjectBase { void update() override { if (auto function = ptr.get()) { - secondaryColour = colourFromHexArray(function->x_bgcolor).toString(); - primaryColour = colourFromHexArray(function->x_fgcolor).toString(); + secondaryColour = colourToVar(colourFromHexArray(function->x_bgcolor)); + primaryColour = colourToVar(colourFromHexArray(function->x_fgcolor)); sizeProperty = VarArray { var(function->x_width), var(function->x_height) }; initialise = function->x_init; @@ -106,44 +106,44 @@ class FunctionObject final : public ObjectBase { bool const editing = cnv->locked == var(true) || cnv->presentationMode == var(true) || ModifierKeys::getCurrentModifiers().isCtrlDown(); auto const b = getLocalBounds().toFloat(); - auto const backgroundColour = nvgColour(Colour::fromString(secondaryColour.toString())); + auto const backgroundColour = nvgColour(getValue(secondaryColour)); - auto const foregroundColour = nvgColour(Colour::fromString(primaryColour.toString())); + auto const foregroundColour = nvgColour(getValue(primaryColour)); auto const selectedOutlineColour = nvgColour(PlugDataColours::objectSelectedOutlineColour); auto const outlineColour = nvgColour(PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, selected ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, selected ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); - nvgStrokeColor(nvg, foregroundColour); + nanovg::nvgStrokeColor(nvg, foregroundColour); auto realPoints = getRealPoints(); auto lastPoint = realPoints[0]; for (int i = 1; i < realPoints.size(); i++) { auto newPoint = realPoints[i]; - nvgBeginPath(nvg); - nvgMoveTo(nvg, lastPoint.getX(), lastPoint.getY()); - nvgLineTo(nvg, newPoint.getX(), newPoint.getY()); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, lastPoint.getX(), lastPoint.getY()); + nanovg::nvgLineTo(nvg, newPoint.getX(), newPoint.getY()); + nanovg::nvgStroke(nvg); lastPoint = newPoint; } for (int i = 0; i < realPoints.size(); i++) { auto point = realPoints[i]; // Make sure line isn't visible through the hole - nvgBeginPath(nvg); - nvgFillColor(nvg, backgroundColour); - nvgCircle(nvg, point.getX(), point.getY(), 2.5f); - nvgFill(nvg); - - nvgFillColor(nvg, foregroundColour); - nvgStrokeColor(nvg, hoverIdx == i && editing ? outlineColour : foregroundColour); - nvgBeginPath(nvg); - nvgCircle(nvg, point.getX(), point.getY(), 2.5f); + nanovg::nvgBeginPath(nvg); + nanovg::nvgFillColor(nvg, backgroundColour); + nanovg::nvgCircle(nvg, point.getX(), point.getY(), 2.5f); + nanovg::nvgFill(nvg); + + nanovg::nvgFillColor(nvg, foregroundColour); + nanovg::nvgStrokeColor(nvg, hoverIdx == i && editing ? outlineColour : foregroundColour); + nanovg::nvgBeginPath(nvg); + nanovg::nvgCircle(nvg, point.getX(), point.getY(), 2.5f); if (selectedIdx == i) { - nvgFill(nvg); + nanovg::nvgFill(nvg); } - nvgStrokeWidth(nvg, 1.5f); - nvgStroke(nvg); + nanovg::nvgStrokeWidth(nvg, 1.5f); + nanovg::nvgStroke(nvg); } } @@ -403,10 +403,10 @@ class FunctionObject final : public ObjectBase { object->updateBounds(); } else if (v.refersToSameSourceAs(primaryColour)) { - colourToHexArray(Colour::fromString(primaryColour.toString()), function->x_fgcolor); + colourToHexArray(getValue(primaryColour), function->x_fgcolor); repaint(); } else if (v.refersToSameSourceAs(secondaryColour)) { - colourToHexArray(Colour::fromString(secondaryColour.toString()), function->x_bgcolor); + colourToHexArray(getValue(secondaryColour), function->x_bgcolor); repaint(); } else if (v.refersToSameSourceAs(sendSymbol)) { auto const symbol = sendSymbol.toString(); diff --git a/Source/Objects/GemWindowObject.h b/Source/Objects/GemWindowObject.h index 4ddd05104e..6445f74054 100644 --- a/Source/Objects/GemWindowObject.h +++ b/Source/Objects/GemWindowObject.h @@ -59,10 +59,10 @@ class GemCanvasObject final : public ObjectBase auto const b = getLocalBounds().toFloat(); // Dark background placeholder - nvgDrawRoundedRect(nvg, + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), - nvgRGBf(0.05f, 0.05f, 0.05f), - nvgRGBf(0.15f, 0.15f, 0.15f), + nanovg::nvgRGBf(0.05f, 0.05f, 0.05f), + nanovg::nvgRGBf(0.15f, 0.15f, 0.15f), Corners::objectCornerRadius); if (!gemCanvas) @@ -74,12 +74,12 @@ class GemCanvasObject final : public ObjectBase if (gemCanvas->pullPixels(rgbaBuffer, w, h) && !rgbaBuffer.empty()) { if (nvgImage < 0 || w != texW || h != texH) { if (nvgImage >= 0) - nvgDeleteImage(nvg, nvgImage); - nvgImage = nvgCreateImageARGB(nvg, w, h, 0, rgbaBuffer.data()); + nanovg::nvgDeleteImage(nvg, nvgImage); + nvgImage = nanovg::nvgCreateImageARGB(nvg, w, h, 0, rgbaBuffer.data()); texW = w; texH = h; } else { - nvgUpdateImage(nvg, nvgImage, rgbaBuffer.data()); + nanovg::nvgUpdateImage(nvg, nvgImage, rgbaBuffer.data()); } } } @@ -87,12 +87,12 @@ class GemCanvasObject final : public ObjectBase if (nvgImage < 0) return; - NVGpaint paint = nvgImagePattern(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), 0.0f, nvgImage, 1.0f); + NVGpaint paint = nanovg::nvgImagePattern(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), 0.0f, nvgImage, 1.0f); - nvgBeginPath(nvg); - nvgRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius); - nvgFillPaint(nvg, paint); - nvgFill(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius); + nanovg::nvgFillPaint(nvg, paint); + nanovg::nvgFill(nvg); } void timerCallback() override diff --git a/Source/Objects/GraphOnParent.h b/Source/Objects/GraphOnParent.h index a108f8d367..f6a1e07d9b 100644 --- a/Source/Objects/GraphOnParent.h +++ b/Source/Objects/GraphOnParent.h @@ -68,9 +68,9 @@ class GraphTicks final : public ObjectLabel { setLabelColour(PlugDataColours::canvasTextColour); } - void renderLabel(NVGcontext* nvg, float const scale) override + void renderLabel(NVGGraphicsContext& llgc) override { - ignoreUnused(scale); + auto* nvg = llgc.getContext(); if (!isVisible()) return; @@ -81,24 +81,20 @@ class GraphTicks final : public ObjectLabel { auto const x1 = static_cast(localGraphBounds.getX()); auto const x2 = static_cast(localGraphBounds.getRight()); - nvgFontFace(nvg, "Inter-Regular"); - nvgFontSize(nvg, 10.0f); - nvgFillColor(nvg, nvgColour(PlugDataColours::canvasTextColour)); + nanovg::nvgFillColor(nvg, nvgColour(PlugDataColours::canvasTextColour)); for (auto const& text : xLabels) { auto const xpos = jmap(text.getFloatValue(), gl_x1, gl_x2, x1, x2); auto const ypos = jmap(xLabelY, gl_y1, gl_y2, y1, y2); - auto const align = xLabelY > 0.5f * (gl_y1 + gl_y2) ? NVG_ALIGN_BOTTOM : NVG_ALIGN_TOP; - nvgTextAlign(nvg, NVG_ALIGN_CENTER | align); - nvgText(nvg, xpos, ypos, text.toRawUTF8(), nullptr); + auto const align = xLabelY > 0.5f * (gl_y1 + gl_y2) ? Justification::centredBottom : Justification::centredTop; + llgc.drawText(text, Point(xpos, ypos), align); } for (auto const& text : yLabels) { auto const xpos = jmap(yLabelX, gl_x1, gl_x2, x1, x2); auto const ypos = jmap(text.getFloatValue(), gl_y1, gl_y2, y1, y2); - auto const align = yLabelX > 0.5f * (gl_x1 + gl_x2) ? NVG_ALIGN_LEFT : NVG_ALIGN_RIGHT; - nvgTextAlign(nvg, NVG_ALIGN_MIDDLE | align); - nvgText(nvg, xpos, ypos, text.toRawUTF8(), nullptr); + auto const align = yLabelX > 0.5f * (gl_x1 + gl_x2) ? Justification::centredLeft : Justification::centredRight; + llgc.drawText(text, Point(xpos, ypos), align); } } @@ -111,30 +107,30 @@ class GraphTicks final : public ObjectLabel { for (int i = 0; f < 0.99f * gl_x2 + 0.01f * gl_x1; i++, f += xTickInc) { auto const xpos = jmap(f, gl_x2, gl_x1, x1, x2); int const tickpix = i % xTicksPerBig ? 2 : 4; - nvgBeginPath(nvg); - nvgMoveTo(nvg, xpos, y2); - nvgLineTo(nvg, xpos, y2 - tickpix); - nvgStroke(nvg); - - nvgBeginPath(nvg); - nvgMoveTo(nvg, xpos, y1); - nvgLineTo(nvg, xpos, y1 + tickpix); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, xpos, y2); + nanovg::nvgLineTo(nvg, xpos, y2 - tickpix); + nanovg::nvgStroke(nvg); + + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, xpos, y1); + nanovg::nvgLineTo(nvg, xpos, y1 + tickpix); + nanovg::nvgStroke(nvg); } f = xTickPoint - xTickInc; for (int i = 1; f > 0.99f * gl_x1 + 0.01f * gl_x2; i++, f -= xTickInc) { auto const xpos = jmap(f, gl_x2, gl_x1, x1, x2); int const tickpix = i % xTicksPerBig ? 2 : 4; - nvgBeginPath(nvg); - nvgMoveTo(nvg, xpos, y2); - nvgLineTo(nvg, xpos, y2 - tickpix); - nvgStroke(nvg); - - nvgBeginPath(nvg); - nvgMoveTo(nvg, xpos, y1); - nvgLineTo(nvg, xpos, y1 + tickpix); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, xpos, y2); + nanovg::nvgLineTo(nvg, xpos, y2 - tickpix); + nanovg::nvgStroke(nvg); + + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, xpos, y1); + nanovg::nvgLineTo(nvg, xpos, y1 + tickpix); + nanovg::nvgStroke(nvg); } } @@ -143,30 +139,30 @@ class GraphTicks final : public ObjectLabel { for (int i = 0; f < 0.99f * gl_y1 + 0.01f * gl_y2; i++, f += yTickInc) { auto const ypos = jmap(f, gl_y2, gl_y1, y1, y2); int const tickpix = i % yTicksPerBig ? 2 : 4; - nvgBeginPath(nvg); - nvgMoveTo(nvg, x1, ypos); - nvgLineTo(nvg, x1 + tickpix, ypos); - nvgStroke(nvg); - - nvgBeginPath(nvg); - nvgMoveTo(nvg, x2, ypos); - nvgLineTo(nvg, x2 - tickpix, ypos); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, x1, ypos); + nanovg::nvgLineTo(nvg, x1 + tickpix, ypos); + nanovg::nvgStroke(nvg); + + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, x2, ypos); + nanovg::nvgLineTo(nvg, x2 - tickpix, ypos); + nanovg::nvgStroke(nvg); } f = yTickPoint - yTickInc; for (int i = 1; f > 0.99f * gl_y2 + 0.01f * gl_y1; i++, f -= yTickInc) { auto const ypos = jmap(f, gl_y2, gl_y1, y1, y2); int const tickpix = i % yTicksPerBig ? 2 : 4; - nvgBeginPath(nvg); - nvgMoveTo(nvg, x1, ypos); - nvgLineTo(nvg, x1 + tickpix, ypos); - nvgStroke(nvg); - - nvgBeginPath(nvg); - nvgMoveTo(nvg, x2, ypos); - nvgLineTo(nvg, x2 - tickpix, ypos); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, x1, ypos); + nanovg::nvgLineTo(nvg, x1 + tickpix, ypos); + nanovg::nvgStroke(nvg); + + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, x2, ypos); + nanovg::nvgLineTo(nvg, x2 - tickpix, ypos); + nanovg::nvgStroke(nvg); } } } @@ -183,8 +179,6 @@ class GraphOnParent final : public ObjectBase { pd::Patch::Ptr subpatch; std::unique_ptr canvas; - CachedTextRender textRenderer; - NVGImage openInGopBackground; std::unique_ptr editor; @@ -304,10 +298,6 @@ class GraphOnParent final : public ObjectBase { editor->setBounds(getLocalBounds().removeFromTop(18)); } - auto text = getText(); - if (!getValue(hideNameAndArgs) && text != "graph" && text.isNotEmpty()) { - textRenderer.prepareLayout(getText(), Fonts::getDefaultFont().withHeight(13), PlugDataColours::canvasTextColour, getWidth(), getWidth(), false); - } updateCanvas(); updateDrawables(); @@ -317,10 +307,6 @@ class GraphOnParent final : public ObjectBase { void lookAndFeelChanged() override { - auto text = getText(); - if (!getValue(hideNameAndArgs) && text != "graph" && text.isNotEmpty()) { - textRenderer.prepareLayout(getText(), Fonts::getDefaultFont().withHeight(13), PlugDataColours::canvasTextColour, getWidth(), getWidth(), false); - } } void showEditor() override @@ -494,13 +480,18 @@ class GraphOnParent final : public ObjectBase { { // Strangly, the title goes below the graph content in pd if (!getValue(hideNameAndArgs)) { + auto& llgc = *cnv->editor->getNanoLLGC(); if (editor && editor->isVisible()) { - Graphics g(*cnv->editor->getNanoLLGC()); - editor->paintEntireComponent(g, true); + llgc.renderComponent(*editor); } else { auto const text = getText(); - if (!getValue(hideNameAndArgs) && text != "graph" && text.isNotEmpty()) { - textRenderer.renderText(nvg, Rectangle(5, 1, getWidth() - 5, 16), getImageScale()); + if (text != "graph" && text.isNotEmpty()) { + Graphics g(llgc); + auto const textBounds = Rectangle(5, 1, getWidth() - 5, 16); + NVGGraphicsContext::ScopedAnchoredDraw anchor(llgc, textBounds); + g.setFont(Fonts::getDefaultFont().withHeight(13)); + g.setColour(PlugDataColours::canvasTextColour); + g.drawText(text, textBounds, Justification::centredLeft); } } } @@ -517,8 +508,8 @@ class GraphOnParent final : public ObjectBase { invalidArea = canvas->getLocalArea(cnv, invalidArea).expanded(1); NVGScopedState scopedState(nvg); - nvgIntersectRoundedScissor(nvg, b.getX() + 0.75f, b.getY() + 0.75f, b.getWidth() - 1.5f, b.getHeight() - 1.5f, Corners::objectCornerRadius); - nvgTranslate(nvg, canvas->getX(), canvas->getY()); + nanovg::nvgIntersectRoundedScissor(nvg, b.getX() + 0.75f, b.getY() + 0.75f, b.getWidth() - 1.5f, b.getHeight() - 1.5f, Corners::objectCornerRadius); + nanovg::nvgTranslate(nvg, canvas->getX(), canvas->getY()); canvas->performRender(nvg, invalidArea); } @@ -543,28 +534,23 @@ class GraphOnParent final : public ObjectBase { g.addTransform(rotate.inverted()); }); } - auto const imagePaint = nvgImagePattern(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), 0.0f, openInGopBackground.getImageId(), 1.0f); - nvgBeginPath(nvg); - nvgRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius); - nvgFillPaint(nvg, imagePaint); - nvgFill(nvg); + auto const imagePaint = nanovg::nvgImagePattern(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), 0.0f, openInGopBackground.getImageId(), 1.0f); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius); + nanovg::nvgFillPaint(nvg, imagePaint); + nanovg::nvgFill(nvg); auto const errorText = String("Graph open in split view"); auto const stringLength = Fonts::getStringWidth(errorText, 12); if (stringLength < getWidth() - Object::doubleMargin - 20 /* 20 is a hack for now */ && getHeight() > 12) { - nvgBeginPath(nvg); - nvgFontFace(nvg, "Inter-Regular"); - nvgFontSize(nvg, 12.0f); - nvgFillColor(nvg, nvgColour(PlugDataColours::commentTextColour)); // why comment colour? - nvgTextAlign(nvg, NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER); - nvgText(nvg, b.getCentreX(), b.getCentreY(), errorText.toRawUTF8(), nullptr); + Fonts::drawText(canvas->editor->getNanoLLGC(), errorText, b, Fonts::getCurrentFont().withHeight(12), PlugDataColours::commentTextColour, Justification::centred); } } - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgRGBA(0, 0, 0, 0), nvgColour(object->isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nanovg::nvgRGBA(0, 0, 0, 0), nvgColour(object->isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); - nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); getTicks()->render(nvg, b); } diff --git a/Source/Objects/IEMHelper.h b/Source/Objects/IEMHelper.h index 856b583c5f..b69634af97 100644 --- a/Source/Objects/IEMHelper.h +++ b/Source/Objects/IEMHelper.h @@ -28,16 +28,16 @@ class IEMHelper { void update() { bool colourHasChanged = false; - if (auto const col = getForegroundColour().toString(); col != primaryColour) { - primaryColour = col; + if (auto const col = getForegroundColour(); col != getValue(primaryColour)) { + primaryColour = colourToVar(col); colourHasChanged = true; } - if (auto const col = getBackgroundColour().toString(); col != secondaryColour) { - secondaryColour = col; + if (auto const col = getBackgroundColour(); col != getValue(secondaryColour)) { + secondaryColour = colourToVar(col); colourHasChanged = true; } // we only need the callback that colourHasChanged will trigger for the object ATM. - labelColour = getLabelColour().toString(); + labelColour = colourToVar(getLabelColour()); if (auto iemgui = ptr.get()) { labelPosition = VarArray { var(iemgui->x_ldx), var(iemgui->x_ldy) }; @@ -83,7 +83,7 @@ class IEMHelper { auto setColour = [this](Value& targetValue, pd::Atom const& atom) { if (atom.isSymbol()) { auto const colour = "#FF" + atom.toString().fromFirstOccurrenceOf("#", false, false); - gui->setParameterExcludingListener(targetValue, colour); + gui->setParameterExcludingListener(targetValue, colourToVar(Colour::fromString(colour))); } else { int iemcolor = atom.getFloat(); @@ -98,7 +98,7 @@ class IEMHelper { iemcolor = (-1 - iemcolor) & 0xffffff; auto const colour = convertFromIEMColour(iemcolor); - gui->setParameterExcludingListener(targetValue, colour.toString()); + gui->setParameterExcludingListener(targetValue, colourToVar(colour)); } }; switch (symbol) { @@ -180,17 +180,17 @@ class IEMHelper { setReceiveSymbol(receiveSymbol.toString()); object->updateIolets(); } else if (v.refersToSameSourceAs(primaryColour)) { - auto const colour = Colour::fromString(primaryColour.toString()); + auto const colour = getValue(primaryColour); setForegroundColour(colour); iemColourChangedCallback(); gui->repaint(); } else if (v.refersToSameSourceAs(secondaryColour)) { - auto const colour = Colour::fromString(secondaryColour.toString()); + auto const colour = getValue(secondaryColour); setBackgroundColour(colour); iemColourChangedCallback(); gui->repaint(); } else if (v.refersToSameSourceAs(labelColour)) { - setLabelColour(Colour::fromString(labelColour.toString())); + setLabelColour(getValue(labelColour)); gui->updateLabel(); } else if (v.refersToSameSourceAs(labelPosition)) { setLabelPosition({ labelPosition.getValue().getArray()->getReference(0), labelPosition.getValue().getArray()->getReference(1) }); diff --git a/Source/Objects/KeyboardObject.h b/Source/Objects/KeyboardObject.h index 1cf7c2086c..8f29911cdb 100644 --- a/Source/Objects/KeyboardObject.h +++ b/Source/Objects/KeyboardObject.h @@ -80,13 +80,13 @@ class KeyboardObject final : public ObjectBase auto const outlineColour = nvgColour(selected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); auto const strokeColour = nvgColour(PlugDataColours::guiObjectInternalOutlineColour); - auto const whiteKeyColour = nvgRGB(225, 225, 225); - auto const blackKeyColour = nvgRGB(90, 90, 90); + auto const whiteKeyColour = nanovg::nvgRGB(225, 225, 225); + auto const blackKeyColour = nanovg::nvgRGB(90, 90, 90); auto const activeKeyColour = PlugDataColours::dataColour; - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), whiteKeyColour, outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), whiteKeyColour, outlineColour, Corners::objectCornerRadius); - nvgStrokeColor(nvg, strokeColour); + nanovg::nvgStrokeColor(nvg, strokeColour); auto const whiteNoteWidth = getWhiteKeyWidth(); auto const blackNoteWidth = whiteNoteWidth * 0.7f; @@ -99,66 +99,63 @@ class KeyboardObject final : public ObjectBase // Fill held white notes if (!heldKeys.empty()) { - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); for (auto& key : heldKeys) { if (key < lowest || key >= highest) continue; auto pos = getKeyPosition(key - lowest, true); if (!MidiMessage::isMidiNoteBlack(key)) { - nvgRect(nvg, pos.getStart(), 1.0f, whiteNoteWidth, getHeight() - 2.0f); + nanovg::nvgRect(nvg, pos.getStart(), 1.0f, whiteNoteWidth, getHeight() - 2.0f); } } - nvgFillColor(nvg, nvgColour(activeKeyColour)); - nvgFill(nvg); + nanovg::nvgFillColor(nvg, nvgColour(activeKeyColour)); + nanovg::nvgFill(nvg); } // Draw outlines for white notes - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); for (int i = 1; i < numWhiteNotes; i++) { - nvgMoveTo(nvg, i * whiteNoteWidth, 1.0f); - nvgLineTo(nvg, i * whiteNoteWidth, getHeight() - 1.0f); + nanovg::nvgMoveTo(nvg, i * whiteNoteWidth, 1.0f); + nanovg::nvgLineTo(nvg, i * whiteNoteWidth, getHeight() - 1.0f); } - nvgStroke(nvg); + nanovg::nvgStroke(nvg); // Fill black notes - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); for (int i = 0; i < numBlackNotes; i++) { auto const octave = i / 5 * 12; auto pos = getKeyPosition(blackNotes[i % 5] + octave, true); - nvgRect(nvg, pos.getStart(), 1.0f, blackNoteWidth, blackKeyHeight); + nanovg::nvgRect(nvg, pos.getStart(), 1.0f, blackNoteWidth, blackKeyHeight); } - nvgFillColor(nvg, blackKeyColour); - nvgFill(nvg); + nanovg::nvgFillColor(nvg, blackKeyColour); + nanovg::nvgFill(nvg); // Fill held black notes if (!heldKeys.empty()) { - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); for (auto& key : heldKeys) { if (key < lowest || key >= highest) continue; auto pos = getKeyPosition(key - lowest, true); if (MidiMessage::isMidiNoteBlack(key)) { - nvgRect(nvg, pos.getStart(), 1.0f, blackNoteWidth, blackKeyHeight); + nanovg::nvgRect(nvg, pos.getStart(), 1.0f, blackNoteWidth, blackKeyHeight); } } - nvgFillColor(nvg, nvgColour(activeKeyColour.darker(0.5f))); - nvgFill(nvg); + nanovg::nvgFillColor(nvg, nvgColour(activeKeyColour.darker(0.5f))); + nanovg::nvgFill(nvg); } // Draw octave numbers if (!cnv->locked.getValue() && !cnv->editor->isInPluginMode()) { - nvgFillColor(nvg, nvgRGB(90, 90, 90)); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); auto const fontSizeScaled = b.getHeight() - 2 < 60 ? 13.0f * (b.getHeight() - 2) / 60.0f : 13; - nvgFontSize(nvg, jmax(4.0f, fontSizeScaled)); auto const octaveNumHeight = whiteNoteWidth * 1.2f; auto const scaledHeight = jmin(13.0f, b.getHeight() - 2 < 60 ? octaveNumHeight * (b.getHeight() - 2.0f) / 60.0f : octaveNumHeight); for (int i = 0; i < getValue(octaves); i++) { auto const position = i * 7 * whiteNoteWidth; auto text = String(i + startOctave); auto rectangle = Rectangle(position, b.getHeight() - scaledHeight, whiteNoteWidth, scaledHeight); - nvgText(nvg, rectangle.getCentreX(), rectangle.getCentreY(), text.toRawUTF8(), nullptr); + Fonts::drawText(cnv->editor->getNanoLLGC(), text, rectangle.toFloat(), Fonts::getCurrentFont().withHeight(jmax(4.0f, fontSizeScaled)), Colour(90, 90, 90), Justification::centred); } } } diff --git a/Source/Objects/KnobObject.h b/Source/Objects/KnobObject.h index 6ca67df9e5..852d562caa 100644 --- a/Source/Objects/KnobObject.h +++ b/Source/Objects/KnobObject.h @@ -68,10 +68,10 @@ class Knob final : public Component float const y = centre.getY() + radius * std::sin(angle); // Draw the tick at this position - nvgBeginPath(nvg); - nvgCircle(nvg, x, y, tickRadius); - nvgFillColor(nvg, nvgColour(fgColour)); - nvgFill(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgCircle(nvg, x, y, tickRadius); + nanovg::nvgFillColor(nvg, nvgColour(fgColour)); + nanovg::nvgFill(nvg); } } @@ -270,34 +270,34 @@ class Knob final : public Component auto const arcRadius = arcBounds.getWidth() * 0.5; auto const arcWidth = (arcRadius - lineThickness) / arcRadius; - nvgBeginPath(nvg); - nvgArc(nvg, bounds.getCentreX(), bounds.getCentreY(), arcRadius, startAngle, endAngle, NVG_HOLE); - nvgStrokeWidth(nvg, arcWidth * lineThickness); - nvgStrokeColor(nvg, nvgColour(arcColour)); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgArc(nvg, bounds.getCentreX(), bounds.getCentreY(), arcRadius, startAngle, endAngle, NVG_HOLE); + nanovg::nvgStrokeWidth(nvg, arcWidth * lineThickness); + nanovg::nvgStrokeColor(nvg, nvgColour(arcColour)); + nanovg::nvgStroke(nvg); - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); if (centre < angle) { - nvgArc(nvg, bounds.getCentreX(), bounds.getCentreY(), arcRadius, centre, angle, NVG_HOLE); + nanovg::nvgArc(nvg, bounds.getCentreX(), bounds.getCentreY(), arcRadius, centre, angle, NVG_HOLE); } else { - nvgArc(nvg, bounds.getCentreX(), bounds.getCentreY(), arcRadius, angle, centre, NVG_HOLE); + nanovg::nvgArc(nvg, bounds.getCentreX(), bounds.getCentreY(), arcRadius, angle, centre, NVG_HOLE); } - nvgStrokeColor(nvg, nvgColour(fgColour)); - nvgStrokeWidth(nvg, arcWidth * lineThickness); - nvgStroke(nvg); + nanovg::nvgStrokeColor(nvg, nvgColour(fgColour)); + nanovg::nvgStrokeWidth(nvg, arcWidth * lineThickness); + nanovg::nvgStroke(nvg); } float const wiperX = bounds.getCentreX() + bounds.getWidth() * 0.4f * std::cos(angle); float const wiperY = bounds.getCentreY() + bounds.getWidth() * 0.4f * std::sin(angle); // draw wiper - nvgBeginPath(nvg); - nvgMoveTo(nvg, bounds.getCentreX(), bounds.getCentreY()); // Adjust parameters as needed - nvgLineTo(nvg, wiperX, wiperY); // Adjust parameters as needed - nvgStrokeWidth(nvg, lineThickness); - nvgStrokeColor(nvg, nvgColour(fgColour)); - nvgLineCap(nvg, NVG_ROUND); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, bounds.getCentreX(), bounds.getCentreY()); // Adjust parameters as needed + nanovg::nvgLineTo(nvg, wiperX, wiperY); // Adjust parameters as needed + nanovg::nvgStrokeWidth(nvg, lineThickness); + nanovg::nvgStrokeColor(nvg, nvgColour(fgColour)); + nanovg::nvgLineCap(nvg, NVG_ROUND); + nanovg::nvgStroke(nvg); drawTicks(nvg, bounds, startAngle, endAngle, lineThickness); } @@ -569,10 +569,10 @@ class KnobObject final : public ObjectBase { showArc = knb->x_arc; exponential = knb->x_exp; logMode = knb->x_expmode + 1; - primaryColour = getForegroundColour().toString(); - secondaryColour = getBackgroundColour().toString(); + primaryColour = colourToVar(getForegroundColour()); + secondaryColour = colourToVar(getBackgroundColour()); transparent = knb->x_transparent; - arcColour = getArcColour().toString(); + arcColour = colourToVar(getArcColour()); square = knb->x_square; sizeProperty = knb->x_size; arcStart = knb->x_arcstart; @@ -747,20 +747,20 @@ class KnobObject final : public ObjectBase { break; } case hash("fgcolor"): { - primaryColour = getForegroundColour().toString(); + primaryColour = colourToVar(getForegroundColour()); break; } case hash("bgcolor"): { - secondaryColour = getBackgroundColour().toString(); + secondaryColour = colourToVar(getBackgroundColour()); break; } case hash("colors"): { - primaryColour = getForegroundColour().toString(); - secondaryColour = getBackgroundColour().toString(); + primaryColour = colourToVar(getForegroundColour()); + secondaryColour = colourToVar(getBackgroundColour()); break; } case hash("arccolor"): { - arcColour = getArcColour().toString(); + arcColour = colourToVar(getArcColour()); break; } case hash("init"): { @@ -881,20 +881,20 @@ class KnobObject final : public ObjectBase { { auto const b = getLocalBounds().toFloat(); - auto const background = ::getValue(transparent) ? nvgRGBA(0, 0, 0, 0) : bgCol; + auto const background = ::getValue(transparent) ? nanovg::nvgRGBA(0, 0, 0, 0) : bgCol; if (::getValue(square)) { bool const selected = object->isSelected() && !cnv->isGraph; auto const outlineColour = nvgColour(selected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); auto const lineThickness = std::max(b.getWidth() * 0.03f, 1.0f); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), background, outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), background, outlineColour, Corners::objectCornerRadius); if (!::getValue(showArc)) { - nvgBeginPath(nvg); - nvgStrokeWidth(nvg, lineThickness); - nvgStrokeColor(nvg, nvgColour(::getValue(arcColour))); - nvgCircle(nvg, b.getCentreX(), b.getCentreY(), b.getWidth() / 2.7f); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgStrokeWidth(nvg, lineThickness); + nanovg::nvgStrokeColor(nvg, nvgColour(::getValue(arcColour))); + nanovg::nvgCircle(nvg, b.getCentreX(), b.getCentreY(), b.getWidth() / 2.7f); + nanovg::nvgStroke(nvg); } knob.render(nvg); @@ -909,17 +909,17 @@ class KnobObject final : public ObjectBase { float const scaleOffsetX = originalCentre.x * (1.0f - scaleFactor); float const scaleOffsetY = originalCentre.y * (1.0f - scaleFactor); - nvgTranslate(nvg, scaleOffsetX, scaleOffsetY); - nvgScale(nvg, scaleFactor, scaleFactor); + nanovg::nvgTranslate(nvg, scaleOffsetX, scaleOffsetY); + nanovg::nvgScale(nvg, scaleFactor, scaleFactor); - nvgFillColor(nvg, background); - nvgBeginPath(nvg); - nvgCircle(nvg, circleBounds.getCentreX(), circleBounds.getCentreY(), circleBounds.getWidth() / 2.0f); - nvgFill(nvg); + nanovg::nvgFillColor(nvg, background); + nanovg::nvgBeginPath(nvg); + nanovg::nvgCircle(nvg, circleBounds.getCentreX(), circleBounds.getCentreY(), circleBounds.getWidth() / 2.0f); + nanovg::nvgFill(nvg); - nvgStrokeColor(nvg, nvgColour(PlugDataColours::objectOutlineColour)); - nvgStrokeWidth(nvg, 1.0f); - nvgStroke(nvg); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::objectOutlineColour)); + nanovg::nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgStroke(nvg); knob.render(nvg); } @@ -1148,7 +1148,7 @@ class KnobObject final : public ObjectBase { void updateColours() { - bgCol = nvgColour(Colour::fromString(secondaryColour.toString())); + bgCol = nvgColour(::getValue(secondaryColour)); repaint(); } @@ -1249,13 +1249,13 @@ class KnobObject final : public ObjectBase { sendMessage("receive", { pd::Atom(pd->generateSymbol(receiveSymbol.toString())) }); object->updateIolets(); } else if (value.refersToSameSourceAs(primaryColour)) { - auto const colour = "#" + primaryColour.toString().substring(2); + auto const colour = "#" + ::getValue(primaryColour).toString().substring(2); if (auto knb = ptr.get()) knb->x_fg = pd->generateSymbol(colour); - knob.setFgColour(Colour::fromString(primaryColour.toString())); + knob.setFgColour(::getValue(primaryColour)); updateColours(); } else if (value.refersToSameSourceAs(secondaryColour)) { - auto const colour = "#" + secondaryColour.toString().substring(2); + auto const colour = "#" + ::getValue(secondaryColour).toString().substring(2); if (auto knb = ptr.get()) knb->x_bg = pd->generateSymbol(colour); updateColours(); @@ -1266,10 +1266,10 @@ class KnobObject final : public ObjectBase { updateArcStart(); repaint(); } else if (value.refersToSameSourceAs(arcColour)) { - auto const colour = "#" + arcColour.toString().substring(2); + auto const colour = "#" + ::getValue(arcColour).toString().substring(2); if (auto knb = ptr.get()) knb->x_mg = pd->generateSymbol(colour); - knob.setArcColour(Colour::fromString(arcColour.toString())); + knob.setArcColour(::getValue(arcColour)); repaint(); } else if (value.refersToSameSourceAs(readOnly)) { if (auto knb = ptr.get()) diff --git a/Source/Objects/ListObject.h b/Source/Objects/ListObject.h index b2f98ac704..bb5e10cc7a 100644 --- a/Source/Objects/ListObject.h +++ b/Source/Objects/ListObject.h @@ -166,7 +166,7 @@ class ListObject final : public ObjectBase { auto const sb = b.reduced(0.5f); // Draw background - nvgDrawObjectWithFlag(nvg, sb.getX(), sb.getY(), sb.getWidth(), sb.getHeight(), + nanovg::nvgDrawObjectWithFlag(nvg, sb.getX(), sb.getY(), sb.getWidth(), sb.getHeight(), nvgColour(PlugDataColours::guiObjectBackgroundColour), nvgColour(PlugDataColours::guiObjectBackgroundColour), nvgColour(PlugDataColours::guiObjectBackgroundColour), Corners::objectCornerRadius, ObjectFlagType::FlagTopBottom, PlugDataLook::getUseFlagOutline()); @@ -178,8 +178,8 @@ class ListObject final : public ObjectBase { auto const outlineCol = nvgColour(object->isSelected() || editorActive ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); // Fill the internal of the shape with transparent colour, draw outline & flag with shader - nvgDrawObjectWithFlag(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), - nvgRGBA(0, 0, 0, 0), outlineCol, flagCol, + nanovg::nvgDrawObjectWithFlag(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), + nanovg::nvgRGBA(0, 0, 0, 0), outlineCol, flagCol, Corners::objectCornerRadius, ObjectFlagType::FlagTopBottom, PlugDataLook::getUseFlagOutline()); } diff --git a/Source/Objects/LuaObject.h b/Source/Objects/LuaObject.h index 3d16c7ec6a..f128654802 100644 --- a/Source/Objects/LuaObject.h +++ b/Source/Objects/LuaObject.h @@ -245,7 +245,7 @@ struct LuaPropertiesPanel { }; class LuaObject final : public ObjectBase - , private Value::Listener { + , private Value::Listener, private AsyncUpdater { Colour currentColour; t_symbol* pdluaxSymbol; @@ -260,7 +260,6 @@ class LuaObject final : public ObjectBase int currentTouchIndex = -1; - UnorderedSegmentedMap framebuffers; UnorderedSegmentedMap>> images; struct LuaGuiMessage { @@ -319,7 +318,6 @@ class LuaObject final : public ObjectBase pd->setThis(); } - object->editor->nvgSurface.addBufferedObject(this); parentHierarchyChanged(); } @@ -331,7 +329,6 @@ class LuaObject final : public ObjectBase pd->unlockAudioThread(); zoomScale.removeListener(this); - object->editor->nvgSurface.removeBufferedObject(this); } // We can only attach the zoomscale to canvas after the canvas has been added to its own parent @@ -493,17 +490,29 @@ class LuaObject final : public ObjectBase sendRepaintMessage(); } + void handleAsyncUpdate() override + { + repaint(); + } + void render(NVGcontext* nvg) override { NVGScopedState scopedState(nvg); - auto scale = nvgCurrentPixelScale(nvg) * getValue(zoomScale); - nvgScale(nvg, 1.0f / scale, 1.0f / scale); - nvgTransformQuantize(nvg); + frameSwapLock.enter(); + auto frames = currentFrame; + frameSwapLock.exit(); + - for (auto& [layer, fb] : framebuffers) { - fb.render(nvg, Rectangle(std::ceil(getWidth() * scale), std::ceil(getHeight() * scale))); + auto b = getLocalBounds(); + nanovg::nvgIntersectRoundedScissor(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), Corners::objectCornerRadius); + + for (auto& [layer, layerMessages] : frames) { + for (auto& guiMessage : layerMessages) { + handleGuiMessage(nvg, layer, guiMessage.symbol, guiMessage.size, guiMessage.data.data()); + } } + } void valueChanged(Value& v) override @@ -520,7 +529,7 @@ class LuaObject final : public ObjectBase return; auto getNVGColor = [](uint32_t color) -> NVGcolor { - return nvgRGBA( + return nanovg::nvgRGBA( (color >> 0) & 0xff, (color >> 8) & 0xff, (color >> 16) & 0xff, @@ -535,18 +544,18 @@ class LuaObject final : public ObjectBase NVGcolor ocol = getNVGColor(g->stops[g->nstops - 1].color); float inverse[6]; - nvgTransformInverse(inverse, g->xform); + nanovg::nvgTransformInverse(inverse, g->xform); Point s, e; // Is it always the case that the gradient should be transformed from (0, 0) to (0, 1)? - nvgTransformPoint(&s.x, &s.y, inverse, 0, 0); - nvgTransformPoint(&e.x, &e.y, inverse, 0, 1); + nanovg::nvgTransformPoint(&s.x, &s.y, inverse, 0, 0); + nanovg::nvgTransformPoint(&e.x, &e.y, inverse, 0, 1); NVGpaint paint; if (p->type == NSVG_PAINT_LINEAR_GRADIENT) - paint = nvgLinearGradient(nvg, s.x, s.y, e.x, e.y, icol, ocol); + paint = nanovg::nvgLinearGradient(nvg, s.x, s.y, e.x, e.y, icol, ocol); else - paint = nvgRadialGradient(nvg, s.x, s.y, 0.0, 160, icol, ocol); + paint = nanovg::nvgRadialGradient(nvg, s.x, s.y, 0.0, 160, icol, ocol); return paint; }; @@ -557,62 +566,62 @@ class LuaObject final : public ObjectBase if (!(shape->flags & NSVG_FLAGS_VISIBLE)) continue; - nvgSave(nvg); + nanovg::nvgSave(nvg); // Opacity if (shape->opacity < 1.0) - nvgGlobalAlpha(nvg, shape->opacity); + nanovg::nvgGlobalAlpha(nvg, shape->opacity); // Build path - nvgBeginPath(nvg); - nvgPathWinding(nvg, NVG_NONZERO); + nanovg::nvgBeginPath(nvg); + nanovg::nvgPathWinding(nvg, NVG_NONZERO); // Iterate path linked list for (NSVGpath* path = shape->paths; path; path = path->next) { - nvgMoveTo(nvg, path->pts[0], path->pts[1]); + nanovg::nvgMoveTo(nvg, path->pts[0], path->pts[1]); for (int i = 1; i < path->npts; i += 3) { float* p = &path->pts[2 * i]; - nvgBezierTo(nvg, p[0], p[1], p[2], p[3], p[4], p[5]); + nanovg::nvgBezierTo(nvg, p[0], p[1], p[2], p[3], p[4], p[5]); } // Close path if (path->closed) - nvgClosePath(nvg); + nanovg::nvgClosePath(nvg); } // Fill shape if (shape->fill.type) { switch (shape->fill.type) { case NSVG_PAINT_COLOR: { - nvgFillColor(nvg, getNVGColor(shape->fill.color)); + nanovg::nvgFillColor(nvg, getNVGColor(shape->fill.color)); } break; case NSVG_PAINT_LINEAR_GRADIENT: case NSVG_PAINT_RADIAL_GRADIENT: { - nvgFillPaint(nvg, getPaint(nvg, &shape->fill)); + nanovg::nvgFillPaint(nvg, getPaint(nvg, &shape->fill)); } break; } - nvgFill(nvg); + nanovg::nvgFill(nvg); } // Stroke shape if (shape->stroke.type) { - nvgStrokeWidth(nvg, shape->strokeWidth); + nanovg::nvgStrokeWidth(nvg, shape->strokeWidth); // strokeDashOffset, strokeDashArray, strokeDashCount not yet supported - nvgLineCap(nvg, (NVGlineCap)shape->strokeLineCap); - nvgLineJoin(nvg, (int)shape->strokeLineJoin); + nanovg::nvgLineCap(nvg, (NVGlineCap)shape->strokeLineCap); + nanovg::nvgLineJoin(nvg, (int)shape->strokeLineJoin); switch (shape->stroke.type) { case NSVG_PAINT_COLOR: { - nvgStrokeColor(nvg, getNVGColor(shape->stroke.color)); + nanovg::nvgStrokeColor(nvg, getNVGColor(shape->stroke.color)); } break; case NSVG_PAINT_LINEAR_GRADIENT: { - nvgStrokePaint(nvg, getPaint(nvg, &shape->stroke)); + nanovg::nvgStrokePaint(nvg, getPaint(nvg, &shape->stroke)); } break; } - nvgStroke(nvg); + nanovg::nvgStroke(nvg); } - nvgRestore(nvg); + nanovg::nvgRestore(nvg); } } @@ -625,33 +634,12 @@ class LuaObject final : public ObjectBase if (getLocalBounds().isEmpty()) break; - auto const pixelScale = nvgCurrentPixelScale(nvg); - auto const zoom = getValue(zoomScale); - auto const imageScale = zoom * pixelScale; - int const imageWidth = std::ceil(getWidth() * imageScale); - int const imageHeight = std::ceil(getHeight() * imageScale); - if (!imageWidth || !imageHeight) - return; - - framebuffers[layer].bind(nvg, imageWidth, imageHeight); + nanovg::nvgSave(nvg); - nvgViewport(0, 0, imageWidth, imageHeight); - nvgClear(nvg); - nvgBeginFrame(nvg, getWidth() * zoom, getHeight() * zoom, pixelScale); - nvgScale(nvg, zoom, zoom); - nvgSave(nvg); return; } case hash("lua_end_paint"): { - if (!framebuffers[layer].isValid()) - return; - - auto const pixelScale = nvgCurrentPixelScale(nvg); - auto const scale = getValue(zoomScale) * pixelScale; - nvgGlobalScissor(nvg, 0, 0, getWidth() * scale, getHeight() * scale); - nvgEndFrame(nvg); - framebuffers[layer].unbind(); - repaint(); + nanovg::nvgRestore(nvg); return; } default: @@ -666,8 +654,8 @@ class LuaObject final : public ObjectBase currentColour = StackArray { PlugDataColours::guiObjectBackgroundColour, PlugDataColours::canvasTextColour, PlugDataColours::guiObjectInternalOutlineColour }[colourID]; - nvgFillColor(nvg, nvgColour(currentColour)); - nvgStrokeColor(nvg, nvgColour(currentColour)); + nanovg::nvgFillColor(nvg, nvgColour(currentColour)); + nanovg::nvgStrokeColor(nvg, nvgColour(currentColour)); } if (argc >= 3) { Colour const color(static_cast(atom_getfloat(argv)), @@ -675,8 +663,8 @@ class LuaObject final : public ObjectBase static_cast(atom_getfloat(argv + 2))); currentColour = color.withAlpha(argc >= 4 ? atom_getfloat(argv + 3) : 1.0f); - nvgFillColor(nvg, nvgColour(currentColour)); - nvgStrokeColor(nvg, nvgColour(currentColour)); + nanovg::nvgFillColor(nvg, nvgColour(currentColour)); + nanovg::nvgStrokeColor(nvg, nvgColour(currentColour)); } break; } @@ -688,11 +676,11 @@ class LuaObject final : public ObjectBase float const y2 = atom_getfloat(argv + 3); float const lineThickness = atom_getfloat(argv + 4); - nvgStrokeWidth(nvg, lineThickness); - nvgBeginPath(nvg); - nvgMoveTo(nvg, x1, y1); - nvgLineTo(nvg, x2, y2); - nvgStroke(nvg); + nanovg::nvgStrokeWidth(nvg, lineThickness); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, x1, y1); + nanovg::nvgLineTo(nvg, x2, y2); + nanovg::nvgStroke(nvg); } break; } @@ -703,9 +691,9 @@ class LuaObject final : public ObjectBase float const w = atom_getfloat(argv + 2); float const h = atom_getfloat(argv + 3); - nvgBeginPath(nvg); - nvgEllipse(nvg, x + w / 2, y + h / 2, w / 2, h / 2); - nvgFill(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgEllipse(nvg, x + w / 2, y + h / 2, w / 2, h / 2); + nanovg::nvgFill(nvg); } break; } @@ -717,10 +705,10 @@ class LuaObject final : public ObjectBase float const h = atom_getfloat(argv + 3); float const lineThickness = atom_getfloat(argv + 4); - nvgStrokeWidth(nvg, lineThickness); - nvgBeginPath(nvg); - nvgEllipse(nvg, x + w / 2, y + h / 2, w / 2, h / 2); - nvgStroke(nvg); + nanovg::nvgStrokeWidth(nvg, lineThickness); + nanovg::nvgBeginPath(nvg); + nanovg::nvgEllipse(nvg, x + w / 2, y + h / 2, w / 2, h / 2); + nanovg::nvgStroke(nvg); } break; } @@ -731,7 +719,7 @@ class LuaObject final : public ObjectBase float const w = atom_getfloat(argv + 2); float const h = atom_getfloat(argv + 3); - nvgFillRect(nvg, x, y, w, h); + nanovg::nvgFillRect(nvg, x, y, w, h); } break; } @@ -743,8 +731,8 @@ class LuaObject final : public ObjectBase float const h = atom_getfloat(argv + 3); float const lineThickness = atom_getfloat(argv + 4); - nvgStrokeWidth(nvg, lineThickness); - nvgStrokeRect(nvg, x, y, w, h); + nanovg::nvgStrokeWidth(nvg, lineThickness); + nanovg::nvgStrokeRect(nvg, x, y, w, h); } break; } @@ -756,7 +744,7 @@ class LuaObject final : public ObjectBase float const h = atom_getfloat(argv + 3); float const cornerRadius = atom_getfloat(argv + 4); - nvgFillRoundedRect(nvg, x, y, w, h, cornerRadius); + nanovg::nvgFillRoundedRect(nvg, x, y, w, h, cornerRadius); } break; } @@ -770,10 +758,10 @@ class LuaObject final : public ObjectBase float const cornerRadius = atom_getfloat(argv + 4); float const lineThickness = atom_getfloat(argv + 5); - nvgStrokeWidth(nvg, lineThickness); - nvgBeginPath(nvg); - nvgRoundedRect(nvg, x, y, w, h, cornerRadius); - nvgStroke(nvg); + nanovg::nvgStrokeWidth(nvg, lineThickness); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRoundedRect(nvg, x, y, w, h, cornerRadius); + nanovg::nvgStroke(nvg); } break; } @@ -785,11 +773,11 @@ class LuaObject final : public ObjectBase float const y2 = atom_getfloat(argv + 3); float const lineThickness = atom_getfloat(argv + 4); - nvgStrokeWidth(nvg, lineThickness); - nvgBeginPath(nvg); - nvgMoveTo(nvg, x1, y1); - nvgLineTo(nvg, x2, y2); - nvgStroke(nvg); + nanovg::nvgStrokeWidth(nvg, lineThickness); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, x1, y1); + nanovg::nvgLineTo(nvg, x2, y2); + nanovg::nvgStroke(nvg); } break; } @@ -800,16 +788,17 @@ class LuaObject final : public ObjectBase float const w = atom_getfloat(argv + 3); float const fontHeight = atom_getfloat(argv + 4); int const alignment = atom_getfloat(argv + 5); + auto const text = String::fromUTF8(atom_getsymbol(argv)->s_name); - nvgFontSize(nvg, fontHeight); - nvgTextAlign(nvg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); + float ax = x, ay = y; - float bounds[4]; - nvgTextBoxBounds(nvg, 0, 0, w, atom_getsymbol(argv)->s_name, nullptr, bounds); - float textW = bounds[2] - bounds[0]; // actual rendered width - float textH = bounds[3] - bounds[1]; // actual rendered height (better than fontHeight) + AttributedString txt; + txt.append(text, Fonts::getCurrentFont().withPointHeight(fontHeight), currentColour); - float ax = x, ay = y; + TextLayout layout; + layout.createLayout(txt, w); + auto textW = layout.getWidth(); + auto textH = layout.getHeight(); switch (alignment) { case 1: @@ -841,58 +830,60 @@ class LuaObject final : public ObjectBase break; // TOP } - nvgBeginPath(nvg); - nvgTextBox(nvg, ax, ay, w, atom_getsymbol(argv)->s_name, nullptr); + auto* llgc = cnv->editor->getNanoLLGC(); + Graphics g(*llgc); + NVGGraphicsContext::ScopedAnchoredDraw anchor(*llgc, getLocalBounds().toFloat()); + layout.draw(g, Rectangle(ax, ay, w, textH + 5)); } break; } case hash("lua_fill_path"): { - nvgBeginPath(nvg); - nvgMoveTo(nvg, atom_getfloat(argv), atom_getfloat(argv + 1)); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, atom_getfloat(argv), atom_getfloat(argv + 1)); for (int i = 1; i < argc / 2; i++) { float const x = atom_getfloat(argv + i * 2); float const y = atom_getfloat(argv + i * 2 + 1); - nvgLineTo(nvg, x, y); + nanovg::nvgLineTo(nvg, x, y); } - nvgClosePath(nvg); - nvgFill(nvg); + nanovg::nvgClosePath(nvg); + nanovg::nvgFill(nvg); break; } case hash("lua_stroke_path"): { - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); auto const strokeWidth = atom_getfloat(argv); int const numPoints = (argc - 1) / 2; - nvgMoveTo(nvg, atom_getfloat(argv + 1), atom_getfloat(argv + 2)); + nanovg::nvgMoveTo(nvg, atom_getfloat(argv + 1), atom_getfloat(argv + 2)); for (int i = 1; i < numPoints; i++) { float const x = atom_getfloat(argv + i * 2 + 1); float const y = atom_getfloat(argv + i * 2 + 2); - nvgLineTo(nvg, x, y); + nanovg::nvgLineTo(nvg, x, y); } - nvgStrokeWidth(nvg, strokeWidth); - nvgStroke(nvg); + nanovg::nvgStrokeWidth(nvg, strokeWidth); + nanovg::nvgStroke(nvg); break; } case hash("lua_fill_all"): { auto const bounds = getLocalBounds(); auto const outlineColour = nvgColour(isSelected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), nvgColour(currentColour), outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), nvgColour(currentColour), outlineColour, Corners::objectCornerRadius); // Make sure subsequent draw calls cannot render over the outline - nvgIntersectRoundedScissor(nvg, bounds.getX() + 0.75f, bounds.getY() + 0.75f, bounds.getWidth() - 1.5f, bounds.getHeight() - 1.5f, Corners::objectCornerRadius); + nanovg::nvgIntersectRoundedScissor(nvg, bounds.getX() + 0.75f, bounds.getY() + 0.75f, bounds.getWidth() - 1.5f, bounds.getHeight() - 1.5f, Corners::objectCornerRadius); break; } case hash("lua_draw_svg"): { if (argc >= 3) { float const x = atom_getfloat(argv + 1); float const y = atom_getfloat(argv + 2); - nvgSave(nvg); - nvgTranslate(nvg, x, y); + nanovg::nvgSave(nvg); + nanovg::nvgTranslate(nvg, x, y); drawSVG(nvg, atom_getsymbol(argv)->s_name); - nvgRestore(nvg); + nanovg::nvgRestore(nvg); } break; } @@ -931,10 +922,10 @@ class LuaObject final : public ObjectBase float const x = atom_getfloat(argv + 1); float const y = atom_getfloat(argv + 2); - nvgSave(nvg); - nvgTranslate(nvg, x, y); + nanovg::nvgSave(nvg); + nanovg::nvgTranslate(nvg, x, y); images.at(pathHash).first.render(nvg, images[pathHash].second); - nvgRestore(nvg); + nanovg::nvgRestore(nvg); } break; } @@ -942,7 +933,7 @@ class LuaObject final : public ObjectBase if (argc >= 2) { float const tx = atom_getfloat(argv); float const ty = atom_getfloat(argv + 1); - nvgTranslate(nvg, tx, ty); + nanovg::nvgTranslate(nvg, tx, ty); } break; } @@ -950,13 +941,13 @@ class LuaObject final : public ObjectBase if (argc >= 2) { float const sx = atom_getfloat(argv); float const sy = atom_getfloat(argv + 1); - nvgScale(nvg, sx, sy); + nanovg::nvgScale(nvg, sx, sy); } break; } case hash("lua_reset_transform"): { - nvgRestore(nvg); - nvgSave(nvg); + nanovg::nvgRestore(nvg); + nanovg::nvgSave(nvg); break; } default: @@ -964,30 +955,6 @@ class LuaObject final : public ObjectBase } } - // We need to update the framebuffer in a place where the current graphics context is active (for multi-window support, thanks to Alex for figuring that out) - // but we also need to be outside of calls to beginFrame/endFrame - // So we have this separate callback function that occurs after activating the GPU context, but before starting the frame - void updateFramebuffers(NVGcontext* nvg) override - { - frameSwapLock.enter(); - auto frames = currentFrame; - currentFrame.clear(); - frameSwapLock.exit(); - - for (auto& [layer, layerMessages] : frames) { - for (auto& guiMessage : layerMessages) { - handleGuiMessage(nvg, layer, guiMessage.symbol, guiMessage.size, guiMessage.data.data()); - } - if (!framebuffers[layer].isValid()) - sendRepaintMessage(); - } - - if (isSelected != object->isSelected()) { - isSelected = object->isSelected(); - sendRepaintMessage(); - } - } - ObjectParameters getParameters() override { objectParameters.clear(); @@ -1024,6 +991,7 @@ class LuaObject final : public ObjectBase object->currentFrame[layer] = object->guiCommandBuffer[layer]; object->frameSwapLock.exit(); object->guiCommandBuffer[layer].clear(); + object->triggerAsyncUpdate(); } } } diff --git a/Source/Objects/MessageObject.h b/Source/Objects/MessageObject.h index 855d42d770..28279791fa 100644 --- a/Source/Objects/MessageObject.h +++ b/Source/Objects/MessageObject.h @@ -14,7 +14,7 @@ class MessageObject final : public ObjectBase BorderSize border = BorderSize(1, 4, 1, 1); String objectText; - CachedTextRender textRenderer; + TextLayout layout; bool isDown = false; bool isLocked = false; @@ -72,8 +72,6 @@ class MessageObject final : public ObjectBase fontWidth = glist_fontwidth(cnv->patch.getRawPointer()); } - auto const textSize = textRenderer.getTextBounds(); - // Calculating string width is expensive, so we cache all the strings that we already calculated the width for int const idealWidth = CachedStringWidth<15>::calculateStringWidth(objText) + 13; @@ -89,7 +87,7 @@ class MessageObject final : public ObjectBase textWidth = std::max(charWidth, 2) * fontWidth + offset; } - return { textWidth, textSize.getHeight() }; + return { textWidth, static_cast(layout.getHeight()) }; } void updateTextLayout() @@ -104,10 +102,14 @@ class MessageObject final : public ObjectBase auto const colour = PlugDataColours::canvasTextColour; int const textWidth = getTextSize().getWidth() - 12; - int const widthCache = getValue(sizeProperty); - if (textRenderer.prepareLayout(objText, Fonts::getCurrentFont().withHeight(15), colour, textWidth, widthCache ? widthCache : textWidth, false)) { - repaint(); - } + + AttributedString attributedText(objText); + attributedText.setColour(colour); + attributedText.setFont(Fonts::getCurrentFont().withHeight(15)); + attributedText.setJustification(Justification::centredLeft); + attributedText.setWordWrap(AttributedString::byChar); + layout.createLayout(attributedText, textWidth); + repaint(); } void setPdBounds(Rectangle const b) override @@ -146,7 +148,7 @@ class MessageObject final : public ObjectBase auto const bgCol = nvgColour(isDown ? PlugDataColours::guiObjectInternalOutlineColour : PlugDataColours::guiObjectBackgroundColour); // Draw background - nvgDrawObjectWithFlag(nvg, sb.getX(), sb.getY(), sb.getWidth(), sb.getHeight(), + nanovg::nvgDrawObjectWithFlag(nvg, sb.getX(), sb.getY(), sb.getWidth(), sb.getHeight(), bgCol, bgCol, bgCol, Corners::objectCornerRadius, ObjectFlagType::FlagMessage, PlugDataLook::getUseFlagOutline()); @@ -157,19 +159,22 @@ class MessageObject final : public ObjectBase // We do this by drawing an inner area that is bright, while changing the background colour darker if (isDown) { auto const dB = bounds.reduced(5); - nvgDrawRoundedRect(nvg, dB.getX(), dB.getY(), dB.getWidth(), dB.getHeight(), nvgColour(PlugDataColours::guiObjectBackgroundColour), nvgColour(PlugDataColours::guiObjectBackgroundColour), 0); + nanovg::nvgDrawRoundedRect(nvg, dB.getX(), dB.getY(), dB.getWidth(), dB.getHeight(), nvgColour(PlugDataColours::guiObjectBackgroundColour), nvgColour(PlugDataColours::guiObjectBackgroundColour), 0); } // Draw outline & flag with shader - nvgDrawObjectWithFlag(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), - nvgRGBA(0, 0, 0, 0), outlineCol, flagCol, + nanovg::nvgDrawObjectWithFlag(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), + nanovg::nvgRGBA(0, 0, 0, 0), outlineCol, flagCol, Corners::objectCornerRadius, ObjectFlagType::FlagMessage, PlugDataLook::getUseFlagOutline()); + auto& llgc = *cnv->editor->getNanoLLGC(); if (editor) { - Graphics g(*cnv->editor->getNanoLLGC()); - editor->paintEntireComponent(g, true); + llgc.renderComponent(*editor); } else { - textRenderer.renderText(nvg, border.subtractedFrom(bounds).toFloat(), getImageScale()); + Graphics g(llgc); + auto const textBounds = border.subtractedFrom(bounds).toFloat(); + NVGGraphicsContext::ScopedAnchoredDraw anchor(llgc, textBounds); + layout.draw(g, textBounds); } } diff --git a/Source/Objects/MessboxObject.h b/Source/Objects/MessboxObject.h index 219757a81b..25bba88422 100644 --- a/Source/Objects/MessboxObject.h +++ b/Source/Objects/MessboxObject.h @@ -18,7 +18,6 @@ class MessboxObject final : public ObjectBase Value bold = SynchronousValue(); Value sizeProperty = SynchronousValue(); - NVGImage imageRenderer; bool needsRepaint = true; public: @@ -68,14 +67,14 @@ class MessboxObject final : public ObjectBase { if (auto messbox = ptr.get()) { fontSize = messbox->x_font_size; - primaryColour = Colour(messbox->x_fg[0], messbox->x_fg[1], messbox->x_fg[2]).toString(); - secondaryColour = Colour(messbox->x_bg[0], messbox->x_bg[1], messbox->x_bg[2]).toString(); + primaryColour = colourToVar(Colour(messbox->x_fg[0], messbox->x_fg[1], messbox->x_fg[2])); + secondaryColour = colourToVar(Colour(messbox->x_bg[0], messbox->x_bg[1], messbox->x_bg[2])); sizeProperty = VarArray { var(messbox->x_width), var(messbox->x_height) }; bold = pd->generateSymbol("bold") == messbox->x_font_weight; } auto font = getValue(bold) ? Fonts::getBoldFont() : Fonts::getDefaultFont(); - editor.applyColourToAllText(Colour::fromString(primaryColour.toString())); + editor.applyColourToAllText(getValue(primaryColour)); editor.applyFontToAllText(font.withHeight(getValue(fontSize))); repaint(); @@ -126,20 +125,9 @@ class MessboxObject final : public ObjectBase { bool const selected = object->isSelected() && !cnv->isGraph; auto const outlineColour = selected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour; - nvgDrawRoundedRect(nvg, 0, 0, getWidth(), getHeight(), nvgColour(Colour::fromString(secondaryColour.toString())), nvgColour(outlineColour), Corners::objectCornerRadius); - - auto const scale = getImageScale(); - - if (needsRepaint || isEditorShown() || imageRenderer.needsUpdate(roundToInt(editor.getWidth() * scale), roundToInt(editor.getHeight() * scale))) { - imageRenderer.renderJUCEComponent(nvg, editor, scale); - needsRepaint = false; - } else { - NVGScopedState state(nvg); - nvgScale(nvg, 1.0f / scale, 1.0f / scale); - auto w = roundToInt(scale * static_cast(editor.getWidth())); - auto h = roundToInt(scale * static_cast(editor.getHeight())); - imageRenderer.render(nvg, { 0, 0, w, h }, true); - } + nanovg::nvgDrawRoundedRect(nvg, 0, 0, getWidth(), getHeight(), nvgColour(getValue(secondaryColour)), nvgColour(outlineColour), Corners::objectCornerRadius); + + cnv->editor->getNanoLLGC()->renderComponent(editor); } void paint(Graphics& g) override { } @@ -298,7 +286,7 @@ class MessboxObject final : public ObjectBase object->updateBounds(); } else if (value.refersToSameSourceAs(primaryColour)) { needsRepaint = true; - auto const col = Colour::fromString(primaryColour.toString()); + auto const col = getValue(primaryColour); editor.applyColourToAllText(col); if (auto messbox = ptr.get()) @@ -308,7 +296,7 @@ class MessboxObject final : public ObjectBase } if (value.refersToSameSourceAs(secondaryColour)) { needsRepaint = true; - auto const col = Colour::fromString(secondaryColour.toString()); + auto const col = getValue(secondaryColour); if (auto messbox = ptr.get()) colourToHexArray(col, messbox->x_bg); repaint(); diff --git a/Source/Objects/MousePadObject.h b/Source/Objects/MousePadObject.h index 5d057db32b..572b0237d9 100644 --- a/Source/Objects/MousePadObject.h +++ b/Source/Objects/MousePadObject.h @@ -118,7 +118,7 @@ class MousePadObject final : public ObjectBase { auto const b = getLocalBounds().toFloat(); auto const outlineColour = nvgColour(object->isSelected() && !cnv->isGraph ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), fillColour, outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), fillColour, outlineColour, Corners::objectCornerRadius); } void setPdBounds(Rectangle const b) override diff --git a/Source/Objects/NoteObject.h b/Source/Objects/NoteObject.h index 9f84645509..ee4ba9e784 100644 --- a/Source/Objects/NoteObject.h +++ b/Source/Objects/NoteObject.h @@ -34,8 +34,6 @@ class NoteObject final : public ObjectBase bool wasSelectedOnMouseDown : 1 = false; bool needsRepaint : 1 = false; - NVGImage imageRenderer; - public: NoteObject(pd::WeakReference obj, Object* object) : ObjectBase(obj, object) @@ -129,26 +127,16 @@ class NoteObject final : public ObjectBase void render(NVGcontext* nvg) override { if (getValue(fillBackground) || getValue(outline)) { - auto const fillColour = getValue(fillBackground) ? nvgColour(Colour::fromString(secondaryColour.toString())) : nvgRGBA(0, 0, 0, 0); - auto outlineColour = nvgRGBA(0, 0, 0, 0); + auto const fillColour = getValue(fillBackground) ? nvgColour(getValue(secondaryColour)) : nanovg::nvgRGBA(0, 0, 0, 0); + auto outlineColour = nanovg::nvgRGBA(0, 0, 0, 0); if (getValue(outline)) { bool const selected = object->isSelected() && !cnv->isGraph; outlineColour = nvgColour(selected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); } - nvgDrawRoundedRect(nvg, 0, 0, getWidth(), getHeight(), fillColour, outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, 0, 0, getWidth(), getHeight(), fillColour, outlineColour, Corners::objectCornerRadius); } - auto const scale = getImageScale(); - if (needsRepaint || isEditorShown() || imageRenderer.needsUpdate(roundToInt(getWidth() * scale), roundToInt(getHeight() * scale))) { - imageRenderer.renderJUCEComponent(nvg, noteEditor, scale); - needsRepaint = false; - } else { - NVGScopedState state(nvg); - nvgScale(nvg, 1.0f / scale, 1.0f / scale); - auto w = roundToInt(scale * static_cast(noteEditor.getWidth())); - auto h = roundToInt(scale * static_cast(noteEditor.getHeight())); - imageRenderer.render(nvg, { 0, 0, w, h }, true); - } + cnv->editor->getNanoLLGC()->renderComponent(noteEditor); } void paint(Graphics& g) override { } @@ -157,8 +145,8 @@ class NoteObject final : public ObjectBase { if (auto note = ptr.get()) { currentNoteText = getNote(); - primaryColour = Colour(note->x_red, note->x_green, note->x_blue).toString(); - secondaryColour = Colour(note->x_bg[0], note->x_bg[1], note->x_bg[2]).toString(); + primaryColour = colourToVar(Colour(note->x_red, note->x_green, note->x_blue)); + secondaryColour = colourToVar(Colour(note->x_bg[0], note->x_bg[1], note->x_bg[2])); fontSize = note->x_fontsize; bold = note->x_bold; @@ -191,7 +179,7 @@ class NoteObject final : public ObjectBase noteEditor.setIndents(0, 2); noteEditor.setFont(newFont); noteEditor.setText(currentNoteText); - noteEditor.applyColourToAllText(Colour::fromString(primaryColour.toString())); + noteEditor.applyColourToAllText(getValue(primaryColour)); noteEditor.repaint(); auto const justificationType = getValue(justification); @@ -358,7 +346,7 @@ class NoteObject final : public ObjectBase object->updateBounds(); } else if (v.refersToSameSourceAs(primaryColour)) { - auto const colour = Colour::fromString(primaryColour.toString()); + auto const colour = getValue(primaryColour); noteEditor.applyColourToAllText(colour); if (auto note = ptr.get()) colourToHexArray(colour, ¬e->x_red); @@ -366,7 +354,7 @@ class NoteObject final : public ObjectBase repaint(); } else if (v.refersToSameSourceAs(secondaryColour)) { if (auto note = ptr.get()) - colourToHexArray(Colour::fromString(secondaryColour.toString()), note->x_bg); + colourToHexArray(getValue(secondaryColour), note->x_bg); needsRepaint = true; repaint(); } else if (v.refersToSameSourceAs(fontSize)) { @@ -436,7 +424,7 @@ class NoteObject final : public ObjectBase auto const typefaceName = font.toString(); if (typefaceName.isEmpty() || typefaceName == "Inter") { - return Fonts::getVariableFont().withStyle(style).withHeight(fontHeight); + return Fonts::getCurrentFont().withStyle(style).withHeight(fontHeight); } // Check if a system typeface exists, before we start searching for a font file @@ -513,13 +501,13 @@ class NoteObject final : public ObjectBase } case hash("color"): { if (auto note = ptr.get()) { - primaryColour = Colour(note->x_red, note->x_green, note->x_blue).toString(); + primaryColour = colourToVar(Colour(note->x_red, note->x_green, note->x_blue)); } break; } case hash("bgcolor"): { if (auto note = ptr.get()) { - secondaryColour = Colour(note->x_bg[0], note->x_bg[1], note->x_bg[2]).toString(); + secondaryColour = colourToVar(Colour(note->x_bg[0], note->x_bg[1], note->x_bg[2])); } break; } diff --git a/Source/Objects/NumberObject.h b/Source/Objects/NumberObject.h index b42035fcc2..918bfbe26f 100644 --- a/Source/Objects/NumberObject.h +++ b/Source/Objects/NumberObject.h @@ -371,24 +371,24 @@ class NumberObject final : public ObjectBase { bool const selected = object->isSelected() && !cnv->isGraph; - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundCol, nvgColour(selected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundCol, nvgColour(selected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); constexpr float indent = 9.0f; Rectangle const iconBounds = { (b.getX() + 4.0f), (b.getY() + 4.0f), (indent - 4.0f), (b.getHeight() - 8.0f) }; auto const centreY = iconBounds.getCentreY(); auto const leftX = iconBounds.getX(); - nvgBeginPath(nvg); - nvgMoveTo(nvg, leftX, centreY + 5.0f); - nvgLineTo(nvg, iconBounds.getRight(), centreY); - nvgLineTo(nvg, leftX, centreY - 5.0f); - nvgClosePath(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, leftX, centreY + 5.0f); + nanovg::nvgLineTo(nvg, iconBounds.getRight(), centreY); + nanovg::nvgLineTo(nvg, leftX, centreY - 5.0f); + nanovg::nvgClosePath(nvg); bool const highlighted = hasKeyboardFocus(true) && getValue(object->locked); auto const flagCol = highlighted ? nvgColour(PlugDataColours::objectSelectedOutlineColour) : nvgColour(PlugDataColours::guiObjectInternalOutlineColour); - nvgFillColor(nvg, flagCol); - nvgFill(nvg); + nanovg::nvgFillColor(nvg, flagCol); + nanovg::nvgFill(nvg); input.render(nvg, cnv->editor->getNanoLLGC()); } diff --git a/Source/Objects/NumboxTildeObject.h b/Source/Objects/NumboxTildeObject.h index c86781af3c..41adb67a23 100644 --- a/Source/Objects/NumboxTildeObject.h +++ b/Source/Objects/NumboxTildeObject.h @@ -32,7 +32,7 @@ class NumboxTildeObject final : public ObjectBase { input.onEditorShow = [this]() { - auto const fg = Colour::fromString(primaryColour.toString()); + auto const fg = ::getValue(primaryColour); input.setColour(Label::textWhenEditingColourId, fg); input.setColour(TextEditor::outlineColourId, Colours::transparentBlack); }; @@ -82,8 +82,8 @@ class NumboxTildeObject final : public ObjectBase interval = object->x_rate; ramp = object->x_ramp_ms; init = object->x_set_val; - primaryColour = "ff" + String::fromUTF8(object->x_fg->s_name + 1); - secondaryColour = "ff" + String::fromUTF8(object->x_bg->s_name + 1); + primaryColour = colourToVar(Colour::fromString("ff" + String::fromUTF8(object->x_fg->s_name + 1))); + secondaryColour = colourToVar(Colour::fromString("ff" + String::fromUTF8(object->x_bg->s_name + 1))); mode = object->x_outmode; sizeProperty = VarArray { var(object->x_width), var(object->x_height) }; } @@ -215,9 +215,9 @@ class NumboxTildeObject final : public ObjectBase nbx->x_set_val = ::getValue(init); } } else if (value.refersToSameSourceAs(primaryColour)) { - setForegroundColour(primaryColour.toString()); + setForegroundColour(::getValue(primaryColour).toString()); } else if (value.refersToSameSourceAs(secondaryColour)) { - setBackgroundColour(secondaryColour.toString()); + setBackgroundColour(::getValue(secondaryColour).toString()); } } @@ -242,15 +242,15 @@ class NumboxTildeObject final : public ObjectBase void render(NVGcontext* nvg) override { auto const b = getLocalBounds().toFloat(); - auto const backgroundColour = Colour::fromString(secondaryColour.toString()); + auto const backgroundColour = ::getValue(secondaryColour); bool const selected = object->isSelected() && !cnv->isGraph; auto const outlineColour = selected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour; - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgColour(backgroundColour), nvgColour(outlineColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgColour(backgroundColour), nvgColour(outlineColour), Corners::objectCornerRadius); { NVGScopedState scopedState(nvg); - nvgTranslate(nvg, input.getX(), input.getY()); + nanovg::nvgTranslate(nvg, input.getX(), input.getY()); input.render(nvg, cnv->editor->getNanoLLGC()); } } diff --git a/Source/Objects/ObjectBase.cpp b/Source/Objects/ObjectBase.cpp index e239bce76c..6785511041 100644 --- a/Source/Objects/ObjectBase.cpp +++ b/Source/Objects/ObjectBase.cpp @@ -39,7 +39,6 @@ void canvas_click(t_canvas* x, t_floatarg xpos, t_floatarg ypos, t_floatarg shif #include "TabComponent.h" #include "Pd/Patch.h" #include "Sidebar/Sidebar.h" -#include "Utility/CachedTextRender.h" #include "Utility/CachedStringWidth.h" #include "Dialogs/Dialogs.h" diff --git a/Source/Objects/ObjectBase.h b/Source/Objects/ObjectBase.h index c18bffda9d..ffdba061e4 100644 --- a/Source/Objects/ObjectBase.h +++ b/Source/Objects/ObjectBase.h @@ -14,7 +14,7 @@ #include "Utility/SynchronousValue.h" #include "NVGSurface.h" #include "Utility/NVGUtils.h" -#include "Utility/CachedTextRender.h" +#include "Utility/NVGGraphicsContext.h" #include "Object.h" #include "Canvas.h" @@ -30,11 +30,6 @@ class Object; class ObjectLabel : public Label , public NVGComponent { - hash32 lastTextHash = 0; - NVGImage image; - float lastScale = 1.0f; - bool updateColour = false; - public: explicit ObjectLabel() : NVGComponent(this) @@ -45,40 +40,39 @@ class ObjectLabel : public Label setEditable(false, false); setInterceptsMouseClicks(false, false); setColour(Label::textColourId, Colours::white); + + setCachedComponentImage(new NVGSurface::InvalidationChecker([this](){ + commandBufferDirty = true; + })); } void setLabelColour(Colour c) { setColour(Label::textColourId, c); - updateColour = true; repaint(); } - virtual void renderLabel(NVGcontext* nvg, float const scale) + void render(NVGGraphicsContext& llgc) { - auto const w = roundToInt(scale * static_cast(getWidth())); - auto const h = roundToInt(scale * static_cast(getHeight())); - - auto const textHash = hash(getText()); - if (image.needsUpdate(w, h) || updateColour || lastTextHash != textHash || lastScale != scale) { - updateImage(nvg, scale); - lastTextHash = textHash; - lastScale = scale; - updateColour = false; - } else { - nvgSave(nvg); - // Need to invert scale to make it render on a pixel grid correctly - nvgScale(nvg, 1.0f / scale, 1.0f / scale); - - image.render(nvg, Rectangle(w, h), true); - nvgRestore(nvg); + auto* nvg = llgc.getContext(); + + if(commandBufferDirty) { + commandBuffer.clear(); + nanovg::ScopedCommandRecorder recorder(nvg, commandBuffer); + renderLabel(llgc); + commandBufferDirty = false; } + + nanovg::replay(nvg, commandBuffer); } - void updateImage(NVGcontext* nvg, float const scale) + + virtual void renderLabel(NVGGraphicsContext& llgc) { - // TODO: use single channel image texture - image.renderJUCEComponent(nvg, *this, scale); + llgc.renderComponent(*this); } + + bool commandBufferDirty : 1 = true; + nanovg::CommandBuffer commandBuffer; }; class ObjectBase : public Component diff --git a/Source/Objects/ObjectParameters.h b/Source/Objects/ObjectParameters.h index 483d0f4e2d..30a94e1ac0 100644 --- a/Source/Objects/ObjectParameters.h +++ b/Source/Objects/ObjectParameters.h @@ -76,7 +76,7 @@ class ObjectParameters { for (auto param : objectParameters) { if (!param.defaultValue.isVoid()) { if (param.type == tColour || param.type == tColourAlpha) { - param.valuePtr->setValue(lnf.findColour(param.defaultValue).toString()); + param.valuePtr->setValue(colourToVar(lnf.findColour(param.defaultValue))); } else if (param.defaultValue.isArray() && param.defaultValue.getArray()->isEmpty()) { return; } else { diff --git a/Source/Objects/OpenFileObject.h b/Source/Objects/OpenFileObject.h index d09514f3c0..ede5853d65 100644 --- a/Source/Objects/OpenFileObject.h +++ b/Source/Objects/OpenFileObject.h @@ -101,9 +101,32 @@ class OpenFileObject final : public TextObjectBase { colour = colour.withRotatedHue(0.5f); int const textWidth = getTextSize().getWidth() - 11; - if (cachedTextRender.prepareLayout(objText, Fonts::getCurrentFont().withHeight(15), colour, textWidth, getValue(sizeProperty), PlugDataLook::getUseSyntaxHighlighting() && isValid)) { - repaint(); + + // Rebuild only when the inputs change: this is called every frame from render(). + auto const textHash = hash(objText); + if (textHash == layoutTextHash && textWidth == lastTextWidth && static_cast(colour.getARGB()) == lastColourARGB) + return; + + layoutTextHash = textHash; + lastTextWidth = textWidth; + lastColourARGB = static_cast(colour.getARGB()); + + auto const font = Fonts::getCurrentFont().withHeight(15); + bool const highlightObjectSyntax = PlugDataLook::getUseSyntaxHighlighting() && isValid; + + AttributedString attributedText; + if (highlightObjectSyntax) { + auto const nameColour = colour.interpolatedWith(PlugDataColours::dataColour, 0.7f); + attributedText = getSyntaxHighlightedString(objText, font, colour, nameColour); + } else { + attributedText = AttributedString(objText); + attributedText.setColour(colour); + attributedText.setFont(font); } + attributedText.setJustification(Justification::centredLeft); + attributedText.setWordWrap(AttributedString::byChar); + textLayout.createLayout(attributedText, textWidth); + repaint(); } String getLinkText() const @@ -122,13 +145,16 @@ class OpenFileObject final : public TextObjectBase { auto const b = getLocalBounds(); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgColour(PlugDataColours::textObjectBackgroundColour), nvgRGBA(0, 0, 0, 0), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgColour(PlugDataColours::textObjectBackgroundColour), nanovg::nvgRGBA(0, 0, 0, 0), Corners::objectCornerRadius); + auto& llgc = *cnv->editor->getNanoLLGC(); if (editor && editor->isVisible()) { - Graphics g(*cnv->editor->getNanoLLGC()); - editor->paintEntireComponent(g, true); + llgc.renderComponent(*editor); } else { - cachedTextRender.renderText(nvg, border.subtractedFrom(b).toFloat(), getImageScale()); + Graphics g(llgc); + auto const textBounds = border.subtractedFrom(b).toFloat(); + NVGGraphicsContext::ScopedAnchoredDraw anchor(llgc, textBounds); + textLayout.draw(g, textBounds); } } diff --git a/Source/Objects/PictureObject.h b/Source/Objects/PictureObject.h index 3253533d43..4f36691f95 100644 --- a/Source/Objects/PictureObject.h +++ b/Source/Objects/PictureObject.h @@ -177,17 +177,13 @@ class PictureObject final : public ObjectBase auto const b = getLocalBounds().toFloat(); NVGScopedState scopedState(nvg); - nvgIntersectScissor(nvg, 0, 0, getWidth(), getHeight()); + nanovg::nvgIntersectScissor(nvg, 0, 0, getWidth(), getHeight()); if (!imageBuffer.isValid()) { - nvgFontSize(nvg, 20); - nvgFontFace(nvg, "Inter-Regular"); - nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgFillColor(nvg, nvgColour(PlugDataColours::canvasTextColour)); - nvgText(nvg, b.getCentreX(), b.getCentreY(), "?", nullptr); + Fonts::drawText(cnv->editor->getNanoLLGC(), "?", b, Fonts::getCurrentFont().withHeight(20), PlugDataColours::canvasTextColour, Justification::centred); } else { NVGScopedState scopedState(nvg); - nvgTranslate(nvg, offsetX, offsetY); + nanovg::nvgTranslate(nvg, offsetX, offsetY); imageBuffer.render(nvg, getLocalBounds()); } @@ -195,7 +191,7 @@ class PictureObject final : public ObjectBase auto const outlineColour = selected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour; if (getValue(outline)) { - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgRGBA(0, 0, 0, 0), nvgColour(outlineColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nanovg::nvgRGBA(0, 0, 0, 0), nvgColour(outlineColour), Corners::objectCornerRadius); } } diff --git a/Source/Objects/PopMenu.h b/Source/Objects/PopMenu.h index dc273b820a..5a66e929a6 100644 --- a/Source/Objects/PopMenu.h +++ b/Source/Objects/PopMenu.h @@ -21,8 +21,6 @@ class PopMenu final : public ObjectBase { Value savestate = SynchronousValue(); Value loadbang = SynchronousValue(); - CachedTextRender textRenderer; - NVGcolor fgCol; NVGcolor bgCol; @@ -59,8 +57,8 @@ class PopMenu final : public ObjectBase { void updateColours() { - bgCol = nvgColour(Colour::fromString(secondaryColour.toString())); - fgCol = nvgColour(Colour::fromString(primaryColour.toString())); + bgCol = nvgColour(getValue(secondaryColour)); + fgCol = nvgColour(getValue(primaryColour)); repaint(); } @@ -130,8 +128,8 @@ class PopMenu final : public ObjectBase { for (int i = 0; i < menu->x_n_items; i++) // Loop for menu items items.add(String::fromUTF8(menu->x_items[i]->s_name)); - primaryColour = convertTclColour(String::fromUTF8(menu->x_fg->s_name)).toString(); - secondaryColour = convertTclColour(String::fromUTF8(menu->x_bg->s_name)).toString(); + primaryColour = colourToVar(convertTclColour(String::fromUTF8(menu->x_fg->s_name))); + secondaryColour = colourToVar(convertTclColour(String::fromUTF8(menu->x_bg->s_name))); sizeProperty = VarArray(menu->x_width, menu->x_height); savestate = menu->x_savestate; loadbang = menu->x_lb; @@ -198,40 +196,44 @@ class PopMenu final : public ObjectBase { void updateTextLayout() { - auto const text = currentItem >= 0 ? currentText : getValue(labelNoSelection); - auto const colour = Colour(fgCol.r, fgCol.g, fgCol.b, fgCol.a); - auto const font = Fonts::getCurrentFont().withHeight(getValue(fontSize) * 1.5f); - - if (textRenderer.prepareLayout(text, font, colour, Fonts::getStringWidth(text, font) + 12, getValue(sizeProperty), false)) { - repaint(); - } + // The label is drawn directly in render() from the current state, so just trigger a repaint. + repaint(); } void render(NVGcontext* nvg) override { auto b = getLocalBounds().toFloat(); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), bgCol, nvgColour(object->isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), bgCol, nvgColour(object->isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); auto textBounds = getLocalBounds().reduced(2).translated(2, 0); if (!textBounds.isEmpty()) { - textRenderer.renderText(nvg, textBounds.toFloat(), getImageScale()); + auto const text = currentItem >= 0 ? currentText : getValue(labelNoSelection); + auto const colour = Colour(fgCol.r, fgCol.g, fgCol.b, fgCol.a); + auto const font = Fonts::getCurrentFont().withHeight(getValue(fontSize) * 1.5f); + + auto& llgc = *cnv->editor->getNanoLLGC(); + Graphics g(llgc); + NVGGraphicsContext::ScopedAnchoredDraw anchor(llgc, textBounds.toFloat()); + g.setFont(font); + g.setColour(colour); + g.drawText(text, textBounds.toFloat(), Justification::centredLeft); } auto const triangleBounds = b.removeFromRight(20).withSizeKeepingCentre(20, std::min(getHeight(), 12)); - nvgStrokeColor(nvg, fgCol); - nvgBeginPath(nvg); - nvgMoveTo(nvg, triangleBounds.getCentreX() - 3, triangleBounds.getY() + 3); - nvgLineTo(nvg, triangleBounds.getCentreX(), triangleBounds.getY()); - nvgLineTo(nvg, triangleBounds.getCentreX() + 3, triangleBounds.getY() + 3); - nvgStroke(nvg); - - nvgBeginPath(nvg); - nvgMoveTo(nvg, triangleBounds.getCentreX() - 3, triangleBounds.getBottom() - 3); - nvgLineTo(nvg, triangleBounds.getCentreX(), triangleBounds.getBottom()); - nvgLineTo(nvg, triangleBounds.getCentreX() + 3, triangleBounds.getBottom() - 3); - nvgStroke(nvg); + nanovg::nvgStrokeColor(nvg, fgCol); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, triangleBounds.getCentreX() - 3, triangleBounds.getY() + 3); + nanovg::nvgLineTo(nvg, triangleBounds.getCentreX(), triangleBounds.getY()); + nanovg::nvgLineTo(nvg, triangleBounds.getCentreX() + 3, triangleBounds.getY() + 3); + nanovg::nvgStroke(nvg); + + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, triangleBounds.getCentreX() - 3, triangleBounds.getBottom() - 3); + nanovg::nvgLineTo(nvg, triangleBounds.getCentreX(), triangleBounds.getBottom()); + nanovg::nvgLineTo(nvg, triangleBounds.getCentreX() + 3, triangleBounds.getBottom() - 3); + nanovg::nvgStroke(nvg); } void propertyChanged(Value& value) override @@ -258,12 +260,12 @@ class PopMenu final : public ObjectBase { sendMessage("receive", { pd->generateSymbol(sendSymbol.toString()) }); object->updateIolets(); } else if (value.refersToSameSourceAs(primaryColour)) { - auto const colour = "#" + primaryColour.toString().substring(2); + auto const colour = "#" + getValue(primaryColour).toString().substring(2); if (auto menu = ptr.get()) menu->x_fg = pd->generateSymbol(colour); updateColours(); } else if (value.refersToSameSourceAs(secondaryColour)) { - auto const colour = "#" + secondaryColour.toString().substring(2); + auto const colour = "#" + getValue(secondaryColour).toString().substring(2); if (auto menu = ptr.get()) menu->x_bg = pd->generateSymbol(colour); updateColours(); @@ -348,13 +350,13 @@ class PopMenu final : public ObjectBase { } case hash("fg"): { if (atoms.size() >= 1 && atoms[0].isSymbol()) { - primaryColour = convertTclColour(atoms[0].toString()).toString(); + primaryColour = colourToVar(convertTclColour(atoms[0].toString())); } break; } case hash("bg"): { if (atoms.size() >= 1 && atoms[0].isSymbol()) { - secondaryColour = convertTclColour(atoms[0].toString()).toString(); + secondaryColour = colourToVar(convertTclColour(atoms[0].toString())); } break; } diff --git a/Source/Objects/RadioObject.h b/Source/Objects/RadioObject.h index 55bc95e12c..5c0ec2863a 100644 --- a/Source/Objects/RadioObject.h +++ b/Source/Objects/RadioObject.h @@ -202,23 +202,23 @@ class RadioObject final : public ObjectBase { auto const selectedOutlineColour = nvgColour(PlugDataColours::objectSelectedOutlineColour); auto const outlineColour = nvgColour(PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgColour(iemHelper.getBackgroundColour()), isSelected ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgColour(iemHelper.getBackgroundColour()), isSelected ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); float const size = isVertical ? static_cast(getHeight()) / numItems : static_cast(getWidth()) / numItems; - nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); - nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::guiObjectInternalOutlineColour)); + nanovg::nvgStrokeWidth(nvg, 1.0f); - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); for (int i = 1; i < numItems; i++) { if (isVertical) { - nvgMoveTo(nvg, 1, i * size); - nvgLineTo(nvg, size - 0.5, i * size); + nanovg::nvgMoveTo(nvg, 1, i * size); + nanovg::nvgLineTo(nvg, size - 0.5, i * size); } else { - nvgMoveTo(nvg, i * size, 1); - nvgLineTo(nvg, i * size, size - 0.5); + nanovg::nvgMoveTo(nvg, i * size, 1); + nanovg::nvgLineTo(nvg, i * size, size - 0.5); } } - nvgStroke(nvg); + nanovg::nvgStroke(nvg); auto const bgColour = ::getValue(iemHelper.secondaryColour); @@ -227,8 +227,8 @@ class RadioObject final : public ObjectBase { float const hoverX = isVertical ? 0 : hoverIdx * size; float const hoverY = isVertical ? hoverIdx * size : 0; auto const hoverBounds = Rectangle(hoverX, hoverY, size, size).reduced(jmin(size * 0.25f, 5)); - nvgFillColor(nvg, nvgColour(hoverColour)); - nvgFillRoundedRect(nvg, hoverBounds.getX(), hoverBounds.getY(), hoverBounds.getWidth(), hoverBounds.getHeight(), Corners::objectCornerRadius / 2.0f); + nanovg::nvgFillColor(nvg, nvgColour(hoverColour)); + nanovg::nvgFillRoundedRect(nvg, hoverBounds.getX(), hoverBounds.getY(), hoverBounds.getWidth(), hoverBounds.getHeight(), Corners::objectCornerRadius / 2.0f); } float const selectionX = isVertical ? 0 : selected * size; @@ -237,8 +237,8 @@ class RadioObject final : public ObjectBase { float const sizeH = isVertical ? getWidth() : getHeight(); auto const selectionBounds = Rectangle(selectionX, selectionY, sizeW, sizeH).reduced(jmin(size * 0.25f, 5)); - nvgFillColor(nvg, nvgColour(::getValue(iemHelper.primaryColour))); - nvgFillRoundedRect(nvg, selectionBounds.getX(), selectionBounds.getY(), selectionBounds.getWidth(), selectionBounds.getHeight(), Corners::objectCornerRadius / 2.0f); + nanovg::nvgFillColor(nvg, nvgColour(::getValue(iemHelper.primaryColour))); + nanovg::nvgFillRoundedRect(nvg, selectionBounds.getX(), selectionBounds.getY(), selectionBounds.getWidth(), selectionBounds.getHeight(), Corners::objectCornerRadius / 2.0f); } void updateAspectRatio() diff --git a/Source/Objects/ScalarObject.h b/Source/Objects/ScalarObject.h index cd0fa75c25..d9738d8081 100644 --- a/Source/Objects/ScalarObject.h +++ b/Source/Objects/ScalarObject.h @@ -147,7 +147,7 @@ class DrawableTemplate : public pd::MessageListener }; class DrawableCurve final : public DrawableTemplate - , public DrawablePath { + , public DrawableComponent, public DrawablePath { t_fake_curve* object; GlobalMouseListener globalMouseListener; @@ -156,6 +156,7 @@ class DrawableCurve final : public DrawableTemplate public: DrawableCurve(t_scalar* s, t_gobj* obj, t_word* data, t_template* templ, Canvas* cnv, int const x, int const y, t_template* parent = nullptr) : DrawableTemplate(s, data, templ, parent, cnv, x, y) + , DrawableComponent(static_cast(*this)) , object(reinterpret_cast(obj)) , globalMouseListener(cnv) { @@ -226,17 +227,17 @@ class DrawableCurve final : public DrawableTemplate setJUCEPath(nvg, p); if (closed) { - nvgClosePath(nvg); + nanovg::nvgClosePath(nvg); - nvgFillColor(nvg, nvgColour(getFill().colour)); - nvgFill(nvg); - nvgStrokeWidth(nvg, getStrokeType().getStrokeThickness()); - nvgStrokeColor(nvg, nvgColour(getStrokeFill().colour)); - nvgStroke(nvg); + nanovg::nvgFillColor(nvg, nvgColour(getFill().colour)); + nanovg::nvgFill(nvg); + nanovg::nvgStrokeWidth(nvg, getStrokeType().getStrokeThickness()); + nanovg::nvgStrokeColor(nvg, nvgColour(getStrokeFill().colour)); + nanovg::nvgStroke(nvg); } else { - nvgStrokeWidth(nvg, getStrokeType().getStrokeThickness()); - nvgStrokeColor(nvg, nvgColour(getStrokeFill().colour)); - nvgStroke(nvg); + nanovg::nvgStrokeWidth(nvg, getStrokeType().getStrokeThickness()); + nanovg::nvgStrokeColor(nvg, nvgColour(getStrokeFill().colour)); + nanovg::nvgStroke(nvg); } } @@ -347,11 +348,10 @@ class DrawableCurve final : public DrawableTemplate }; class DrawableSymbol final : public DrawableTemplate - , public DrawableText { + , public DrawableComponent, public DrawableText { t_fake_drawnumber* object; GlobalMouseListener mouseListener; - CachedTextRender textRenderer; String typeBuffer; float mouseDownValue; @@ -360,6 +360,7 @@ class DrawableSymbol final : public DrawableTemplate public: DrawableSymbol(t_scalar* s, t_gobj* obj, t_word* data, t_template* templ, Canvas* cnv, int const x, int const y, t_template* parent = nullptr) : DrawableTemplate(s, data, templ, parent, cnv, x, y) + , DrawableComponent(static_cast(*this)) , object(reinterpret_cast(obj)) { mouseListener.globalMouseDown = [this](MouseEvent const& e) { @@ -414,13 +415,17 @@ class DrawableSymbol final : public DrawableTemplate void render(NVGcontext* nvg) override { - - auto const scale = canvas->isZooming ? canvas->editor->getRenderScale() * 2.0f : canvas->editor->getRenderScale() * std::max(1.0f, getValue(canvas->zoomScale)); auto const bounds = getBoundingBox().getBoundingBox().toNearestInt(); NVGScopedState scopedState(nvg); - nvgTranslate(nvg, bounds.getX(), bounds.getY()); - textRenderer.prepareLayout(getText(), getFont(), getColour(), getWidth(), getWidth(), false); - textRenderer.renderText(nvg, bounds.withZeroOrigin().toFloat(), scale); + nanovg::nvgTranslate(nvg, bounds.getX(), bounds.getY()); + + auto& llgc = *canvas->editor->getNanoLLGC(); + Graphics g(llgc); + auto const textBounds = bounds.withZeroOrigin().toFloat(); + NVGGraphicsContext::ScopedAnchoredDraw anchor(llgc, textBounds); + g.setFont(getFont()); + g.setColour(getColour()); + g.drawText(getText(), textBounds, Justification::centredLeft); } void handleMouseDown(MouseEvent const& e) @@ -515,7 +520,7 @@ class DrawableSymbol final : public DrawableTemplate } auto const symbolColour = numberToColour(fielddesc_getfloat(&object->x_color, templ, data, 1)); - setColour(symbolColour); + DrawableText::setColour(symbolColour); auto const text = String::fromUTF8(buf); auto const font = getFont(); @@ -530,7 +535,7 @@ class DrawableSymbol final : public DrawableTemplate }; class DrawablePlot final : public DrawableTemplate - , public DrawablePath { + , public DrawableComponent, public DrawablePath { t_fake_curve* object; GlobalMouseListener globalMouseListener; @@ -539,6 +544,7 @@ class DrawablePlot final : public DrawableTemplate public: DrawablePlot(t_scalar* s, t_gobj* obj, t_word* data, t_template* templ, Canvas* cnv, int const x, int const y, t_template* parent = nullptr) : DrawableTemplate(s, data, templ, parent, cnv, x, y) + , DrawableComponent(static_cast(*this)) , object(reinterpret_cast(obj)) , globalMouseListener(cnv) { @@ -615,11 +621,11 @@ class DrawablePlot final : public DrawableTemplate Path const p = getPath(); setJUCEPath(nvg, p); - nvgFillColor(nvg, nvgColour(getFill().colour)); - nvgFill(nvg); - nvgStrokeWidth(nvg, getStrokeType().getStrokeThickness()); - nvgStrokeColor(nvg, nvgColour(getStrokeFill().colour)); - nvgStroke(nvg); + nanovg::nvgFillColor(nvg, nvgColour(getFill().colour)); + nanovg::nvgFill(nvg); + nanovg::nvgStrokeWidth(nvg, getStrokeType().getStrokeThickness()); + nanovg::nvgStrokeColor(nvg, nvgColour(getStrokeFill().colour)); + nanovg::nvgStroke(nvg); } static int readOwnerTemplate(t_fake_plot* x, diff --git a/Source/Objects/ScopeObject.h b/Source/Objects/ScopeObject.h index fa5b7e7ec3..5a14ff05d9 100644 --- a/Source/Objects/ScopeObject.h +++ b/Source/Objects/ScopeObject.h @@ -63,9 +63,9 @@ class ScopeObject final : public ObjectBase bufferSize = scope->x_bufsize; delay = scope->x_delay; samplesPerPoint = scope->x_period; - secondaryColour = colourFromHexArray(scope->x_bg).toString(); - primaryColour = colourFromHexArray(scope->x_fg).toString(); - gridColour = colourFromHexArray(scope->x_gg).toString(); + secondaryColour = colourToVar(colourFromHexArray(scope->x_bg)); + primaryColour = colourToVar(colourFromHexArray(scope->x_fg)); + gridColour = colourToVar(colourFromHexArray(scope->x_gg)); sizeProperty = VarArray { var(scope->x_width), var(scope->x_height) }; auto rcvSym = scope->x_rcv_set ? String::fromUTF8(scope->x_rcv_raw->s_name) : getBinbufSymbol(22); @@ -125,51 +125,51 @@ class ScopeObject final : public ObjectBase auto const outlineColour = nvgColour(object->isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgColour(Colour::fromString(secondaryColour.toString())), outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgColour(getValue(secondaryColour)), outlineColour, Corners::objectCornerRadius); auto const dx = getWidth() * 0.125f; auto const dy = getHeight() * 0.25f; - nvgBeginPath(nvg); - nvgStrokeColor(nvg, nvgColour(Colour::fromString(gridColour.toString()))); - nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgBeginPath(nvg); + nanovg::nvgStrokeColor(nvg, nvgColour(getValue(gridColour))); + nanovg::nvgStrokeWidth(nvg, 1.0f); auto xx = dx; for (int i = 0; i < 7; i++) { - nvgMoveTo(nvg, xx, 1.0f); - nvgLineTo(nvg, xx, getHeight() - 1.0f); + nanovg::nvgMoveTo(nvg, xx, 1.0f); + nanovg::nvgLineTo(nvg, xx, getHeight() - 1.0f); xx += dx; } auto yy = dy; for (int i = 0; i < 3; i++) { - nvgMoveTo(nvg, 1.0f, yy); - nvgLineTo(nvg, getWidth() - 1.0f, yy); + nanovg::nvgMoveTo(nvg, 1.0f, yy); + nanovg::nvgLineTo(nvg, getWidth() - 1.0f, yy); yy += dy; } - nvgStroke(nvg); + nanovg::nvgStroke(nvg); NVGScopedState scopedState(nvg); - nvgIntersectScissor(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight()); + nanovg::nvgIntersectScissor(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight()); if (!(y_buffer.empty() || x_buffer.empty())) { - nvgBeginPath(nvg); - nvgStrokeColor(nvg, nvgColour(Colour::fromString(primaryColour.toString()))); - nvgStrokeWidth(nvg, 2.0f); - nvgLineJoin(nvg, NVG_ROUND); - nvgLineCap(nvg, NVG_ROUND); + nanovg::nvgBeginPath(nvg); + nanovg::nvgStrokeColor(nvg, nvgColour(getValue(primaryColour))); + nanovg::nvgStrokeWidth(nvg, 2.0f); + nanovg::nvgLineJoin(nvg, NVG_ROUND); + nanovg::nvgLineCap(nvg, NVG_ROUND); constexpr float offset = 2.0f; float const w = getWidth() - 4; float const h = getHeight() - 4; - nvgMoveTo(nvg, x_buffer[0] * w + offset, y_buffer[0] * h + offset); + nanovg::nvgMoveTo(nvg, x_buffer[0] * w + offset, y_buffer[0] * h + offset); for (size_t i = 1; i < y_buffer.size(); i++) { - nvgLineTo(nvg, x_buffer[i] * w + offset, y_buffer[i] * h + offset); + nanovg::nvgLineTo(nvg, x_buffer[i] * w + offset, y_buffer[i] * h + offset); } - nvgStroke(nvg); + nanovg::nvgStroke(nvg); } } @@ -266,13 +266,13 @@ class ScopeObject final : public ObjectBase object->updateBounds(); } else if (v.refersToSameSourceAs(primaryColour)) { if (auto scope = ptr.get()) - colourToHexArray(Colour::fromString(primaryColour.toString()), scope->x_fg); + colourToHexArray(getValue(primaryColour), scope->x_fg); } else if (v.refersToSameSourceAs(secondaryColour)) { if (auto scope = ptr.get()) - colourToHexArray(Colour::fromString(secondaryColour.toString()), scope->x_bg); + colourToHexArray(getValue(secondaryColour), scope->x_bg); } else if (v.refersToSameSourceAs(gridColour)) { if (auto scope = ptr.get()) - colourToHexArray(Colour::fromString(gridColour.toString()), scope->x_gg); + colourToHexArray(getValue(gridColour), scope->x_gg); } else if (v.refersToSameSourceAs(bufferSize)) { bufferSize = std::clamp(getValue(bufferSize), 0, SCOPE_MAXBUFSIZE * 4); @@ -318,17 +318,17 @@ class ScopeObject final : public ObjectBase } case hash("fgcolor"): { if (atoms.size() == 3) - setParameterExcludingListener(primaryColour, Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()).toString()); + setParameterExcludingListener(primaryColour, colourToVar(Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()))); break; } case hash("bgcolor"): { if (atoms.size() == 3) - setParameterExcludingListener(secondaryColour, Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()).toString()); + setParameterExcludingListener(secondaryColour, colourToVar(Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()))); break; } case hash("gridcolor"): { if (atoms.size() == 3) - setParameterExcludingListener(gridColour, Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()).toString()); + setParameterExcludingListener(gridColour, colourToVar(Colour(atoms[0].getFloat(), atoms[1].getFloat(), atoms[2].getFloat()))); break; } default: diff --git a/Source/Objects/SliderObject.h b/Source/Objects/SliderObject.h index 55715b7e65..463f181eb9 100644 --- a/Source/Objects/SliderObject.h +++ b/Source/Objects/SliderObject.h @@ -169,8 +169,8 @@ class ReversibleSlider final : public Slider auto const sliderPos = jmap(valueToProportionOfLength(getValue()), 1.0f, 0.0f, b.getY(), b.getHeight() - thumbSize); bounds = Rectangle(b.getWidth(), thumbSize).translated(b.getX(), sliderPos); } - nvgFillColor(nvg, trackColour); - nvgFillRoundedRect(nvg, bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), cornerSize); + nanovg::nvgFillColor(nvg, trackColour); + nanovg::nvgFillRoundedRect(nvg, bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), cornerSize); } std::unique_ptr createAccessibilityHandler() override { return nullptr; }; @@ -262,8 +262,8 @@ class SliderObject final : public ObjectBase { iemHelper.update(); - backgroundColour = nvgColour(Colour::fromString(iemHelper.secondaryColour.toString())); - slider.setTrackColour(Colour::fromString(iemHelper.primaryColour.toString())); + backgroundColour = nvgColour(::getValue(iemHelper.secondaryColour)); + slider.setTrackColour(::getValue(iemHelper.primaryColour)); repaint(); } @@ -365,8 +365,8 @@ class SliderObject final : public ObjectBase { } case hash("color"): { iemHelper.receiveObjectMessage(symbol, atoms); - backgroundColour = nvgColour(Colour::fromString(iemHelper.secondaryColour.toString())); - slider.setTrackColour(Colour::fromString(iemHelper.primaryColour.toString())); + backgroundColour = nvgColour(::getValue(iemHelper.secondaryColour)); + slider.setTrackColour(::getValue(iemHelper.primaryColour)); object->repaint(); break; } @@ -383,7 +383,7 @@ class SliderObject final : public ObjectBase { bool const selected = object->isSelected() && !cnv->isGraph; auto const outlineColour = nvgColour(selected ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, outlineColour, Corners::objectCornerRadius); slider.render(nvg); } diff --git a/Source/Objects/SymbolAtomObject.h b/Source/Objects/SymbolAtomObject.h index b5df4b0f6d..335cce625d 100644 --- a/Source/Objects/SymbolAtomObject.h +++ b/Source/Objects/SymbolAtomObject.h @@ -150,22 +150,19 @@ class SymbolAtomObject final : public ObjectBase auto const sb = b.reduced(0.5f); // reduce size of background to stop AA edges from showing through // Draw background - nvgDrawObjectWithFlag(nvg, sb.getX(), sb.getY(), sb.getWidth(), sb.getHeight(), + nanovg::nvgDrawObjectWithFlag(nvg, sb.getX(), sb.getY(), sb.getWidth(), sb.getHeight(), nvgColour(PlugDataColours::guiObjectBackgroundColour), nvgColour(PlugDataColours::guiObjectBackgroundColour), nvgColour(PlugDataColours::guiObjectBackgroundColour), Corners::objectCornerRadius, ObjectFlagType::FlagTop, PlugDataLook::getUseFlagOutline()); - { - Graphics g(*cnv->editor->getNanoLLGC()); - input.paintEntireComponent(g, true); - } + cnv->editor->getNanoLLGC()->renderComponent(input); bool const highlighted = hasKeyboardFocus(true) && getValue(object->locked); auto const flagCol = highlighted ? nvgColour(PlugDataColours::objectSelectedOutlineColour) : nvgColour(PlugDataColours::guiObjectInternalOutlineColour); auto const outlineCol = nvgColour(object->isSelected() || hasKeyboardFocus(true) ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour); // Fill the internal of the shape with transparent colour, draw outline & flag with shader - nvgDrawObjectWithFlag(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), - nvgRGBA(0, 0, 0, 0), outlineCol, flagCol, + nanovg::nvgDrawObjectWithFlag(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), + nanovg::nvgRGBA(0, 0, 0, 0), outlineCol, flagCol, Corners::objectCornerRadius, ObjectFlagType::FlagTop, PlugDataLook::getUseFlagOutline()); } diff --git a/Source/Objects/TextObject.h b/Source/Objects/TextObject.h index b07d8c24b6..5a98348e9d 100644 --- a/Source/Objects/TextObject.h +++ b/Source/Objects/TextObject.h @@ -143,7 +143,7 @@ class TextObjectBase : public ObjectBase std::unique_ptr editor; BorderSize border = BorderSize(1, 6, 1, 1); - CachedTextRender cachedTextRender; + TextLayout layout; Value sizeProperty = SynchronousValue(); String objectText; @@ -199,7 +199,7 @@ class TextObjectBase : public ObjectBase finalBackgroundColour = nvgColour(bg.contrasting(bg.getBrightness() > 0.5f ? 0.03f : 0.05f)); } - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), finalBackgroundColour, finalOutlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), finalBackgroundColour, finalOutlineColour, Corners::objectCornerRadius); // if the object is valid & iolet area colour is differnet from background colour // draw two non-rounded rectangles at top / bottom @@ -218,20 +218,24 @@ class TextObjectBase : public ObjectBase bool const hasIoletArea = ioletAreaColour.r != bg.getRed() || ioletAreaColour.g != bg.getGreen() || ioletAreaColour.b != bg.getBlue() || ioletAreaColour.a != bg.getAlpha(); if (isValid && hasIoletArea) { - nvgFillColor(nvg, ioletAreaColour); - nvgBeginPath(nvg); - nvgRoundedRectVarying(nvg, 0, 0, getWidth(), 3.5f, Corners::defaultCornerRadius, Corners::defaultCornerRadius, 0.0f, 0.0f); - nvgRoundedRectVarying(nvg, 0, getHeight() - 3.5f, getWidth(), 3.5f, 0.0f, 0.0f, Corners::defaultCornerRadius, Corners::defaultCornerRadius); - nvgFill(nvg); + nanovg::nvgFillColor(nvg, ioletAreaColour); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRoundedRectVarying(nvg, 0, 0, getWidth(), 3.5f, Corners::defaultCornerRadius, Corners::defaultCornerRadius, 0.0f, 0.0f); + nanovg::nvgRoundedRectVarying(nvg, 0, getHeight() - 3.5f, getWidth(), 3.5f, 0.0f, 0.0f, Corners::defaultCornerRadius, Corners::defaultCornerRadius); + nanovg::nvgFill(nvg); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgRGBA(0, 0, 0, 0), outlineCol, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nanovg::nvgRGBA(0, 0, 0, 0), outlineCol, Corners::objectCornerRadius); } + auto& llgc = *cnv->editor->getNanoLLGC(); + if (editor && editor->isVisible()) { - Graphics g(*cnv->editor->getNanoLLGC()); - editor->paintEntireComponent(g, true); + llgc.renderComponent(*editor); } else { - cachedTextRender.renderText(nvg, border.subtractedFrom(b).toFloat(), getImageScale()); + Graphics g(llgc); + auto const textBounds = getLocalBounds().reduced(6, 0).toFloat(); + NVGGraphicsContext::ScopedAnchoredDraw anchor(llgc, textBounds); + layout.draw(g, textBounds); } } @@ -289,8 +293,6 @@ class TextObjectBase : public ObjectBase fontWidth = glist_fontwidth(cnv->patch.getRawPointer()); } - auto const textSize = cachedTextRender.getTextBounds(); - // Calculating string width is expensive, so we cache all the strings that we already calculated the width for int const idealWidth = CachedStringWidth<15>::calculateStringWidth(objText) + 13; @@ -308,7 +310,52 @@ class TextObjectBase : public ObjectBase auto const maxIolets = std::max(object->numInputs, object->numOutputs); textWidth = std::max(textWidth, maxIolets * 18); - return { textWidth, textSize.getHeight() }; + return { textWidth, static_cast(layout.getHeight()) }; + } + + // Builds an AttributedString that colours the tokens of a Pd object's text like the editor does + // (object name, flags, math-expression arguments). Shared by text-based objects so they can build + // their TextLayout directly. + static AttributedString getSyntaxHighlightedString(String const& text, Font const& font, Colour const& colour, Colour const& nameColour) + { + auto attributedText = AttributedString(); + auto lines = StringArray::fromLines(text); + + auto const flagColour = colour.interpolatedWith(PlugDataColours::signalColour, 0.7f); + auto const mathColour = colour.interpolatedWith(Colours::purple, 0.5f); + + bool firstToken = true; + bool hadFlag = false; + bool mathExpression = false; + for (int i = 0; i < lines.size(); i++) { + auto& line = lines[i]; + auto tokens = StringArray::fromTokens(line, true); + for (int j = 0; j < tokens.size(); j++) { + auto token = tokens[j]; + if (j != tokens.size() - 1) + token += " "; + if (firstToken) { + attributedText.append(token, font, nameColour); + if (token == "expr " || token == "expr~ " || token == "fexpr~ " || token == "op " || token == "op~ ") { + mathExpression = true; + } + firstToken = false; + } else if (mathExpression) { + attributedText.append(token, font, mathColour); + } else if (token.startsWith("-") && !token.containsOnly("e.-0123456789 ")) { + attributedText.append(token, font, flagColour); + hadFlag = true; + } else if (hadFlag) { + attributedText.append(token, font, nameColour); + } else { + attributedText.append(token, font, colour); + } + } + if (i != lines.size() - 1) + attributedText.append("\n", font, colour); + } + + return attributedText; } virtual void updateTextLayout() @@ -322,10 +369,21 @@ class TextObjectBase : public ObjectBase } auto const colour = PlugDataColours::canvasTextColour; - int const textWidth = getTextSize().getWidth() - 12; - if (cachedTextRender.prepareLayout(objText, Fonts::getCurrentFont().withHeight(15), colour, textWidth, getValue(sizeProperty), PlugDataLook::getUseSyntaxHighlighting() && isValid)) { - repaint(); + auto const font = Fonts::getCurrentFont().withHeight(15); + bool const highlightObjectSyntax = PlugDataLook::getUseSyntaxHighlighting() && isValid; + + AttributedString attributedText; + if (highlightObjectSyntax) { + auto const nameColour = colour.interpolatedWith(PlugDataColours::dataColour, 0.7f); + attributedText = getSyntaxHighlightedString(objText, font, colour, nameColour); + } else { + attributedText = AttributedString(objText); + attributedText.setColour(colour); + attributedText.setFont(font); } + attributedText.setJustification(Justification::centredLeft); + attributedText.setWordWrap(AttributedString::byChar); + layout.createLayout(attributedText, getTextSize().getWidth() - 12); } void setPdBounds(Rectangle const b) override diff --git a/Source/Objects/ToggleObject.h b/Source/Objects/ToggleObject.h index fd54c6e125..fa4db65acd 100644 --- a/Source/Objects/ToggleObject.h +++ b/Source/Objects/ToggleObject.h @@ -86,7 +86,7 @@ class ToggleObject final : public ObjectBase { auto const selectedOutlineColour = nvgColour(PlugDataColours::objectSelectedOutlineColour); auto const outlineColour = nvgColour(PlugDataColours::objectOutlineColour); - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, object->isSelected() ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), backgroundColour, object->isSelected() ? selectedOutlineColour : outlineColour, Corners::objectCornerRadius); auto const sizeReduction = std::min(1.0f, getWidth() / 20.0f); float const margin = (getWidth() * 0.08f + 4.5f) * sizeReduction; @@ -95,14 +95,14 @@ class ToggleObject final : public ObjectBase { auto const max = std::max(crossBounds.getWidth(), crossBounds.getHeight()); auto const strokeWidth = std::max(max * 0.15f, 2.0f) * sizeReduction; - nvgBeginPath(nvg); - nvgMoveTo(nvg, crossBounds.getX(), crossBounds.getY()); - nvgLineTo(nvg, crossBounds.getRight(), crossBounds.getBottom()); - nvgMoveTo(nvg, crossBounds.getRight(), crossBounds.getY()); - nvgLineTo(nvg, crossBounds.getX(), crossBounds.getBottom()); - nvgStrokeColor(nvg, toggleState ? toggledColour : untoggledColour); - nvgStrokeWidth(nvg, strokeWidth); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgMoveTo(nvg, crossBounds.getX(), crossBounds.getY()); + nanovg::nvgLineTo(nvg, crossBounds.getRight(), crossBounds.getBottom()); + nanovg::nvgMoveTo(nvg, crossBounds.getRight(), crossBounds.getY()); + nanovg::nvgLineTo(nvg, crossBounds.getX(), crossBounds.getBottom()); + nanovg::nvgStrokeColor(nvg, toggleState ? toggledColour : untoggledColour); + nanovg::nvgStrokeWidth(nvg, strokeWidth); + nanovg::nvgStroke(nvg); } void toggleObject(Point position) override diff --git a/Source/Objects/VUMeterObject.h b/Source/Objects/VUMeterObject.h index 205a4d2835..34dbe3ea8f 100644 --- a/Source/Objects/VUMeterObject.h +++ b/Source/Objects/VUMeterObject.h @@ -53,8 +53,10 @@ class VUScale final : public ObjectLabel { } } - void renderLabel(NVGcontext* nvg, float const scale) override + void renderLabel(NVGGraphicsContext& llgc) override { + auto* nvg = llgc.getContext(); + if (!isVisible()) return; @@ -66,8 +68,8 @@ class VUScale final : public ObjectLabel { if (decimScaleText && !(scaleDecim & 1 << i)) continue; float const scaleTextYPos = static_cast(i) * (getHeight() - 20) / 10.0f; - nvgFillPaint(nvg, nvgImageAlphaPattern(nvg, 0, scaleTextYPos, getWidth(), 20, 0, scaleImages[i].getImageId(), labelColor)); - nvgFillRect(nvg, 0, scaleTextYPos, getWidth(), 20); + nanovg::nvgFillPaint(nvg, nanovg::nvgImageAlphaPattern(nvg, 0, scaleTextYPos, getWidth(), 20, 0, scaleImages[i].getImageId(), labelColor)); + nanovg::nvgFillRect(nvg, 0, scaleTextYPos, getWidth(), 20); } } }; @@ -93,7 +95,7 @@ class VUMeterObject final : public ObjectBase { updateLabel(); iemHelper.iemColourChangedCallback = [this] { - bgCol = nvgColour(Colour::fromString(iemHelper.secondaryColour.toString())); + bgCol = nvgColour(getValue(iemHelper.secondaryColour)); }; } @@ -215,7 +217,7 @@ class VUMeterObject final : public ObjectBase { auto const b = getLocalBounds(); auto const bS = b.reduced(0.5f); // Object background - nvgDrawRoundedRect(nvg, bS.getX(), bS.getY(), bS.getWidth(), bS.getHeight(), bgCol, bgCol, Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, bS.getX(), bS.getY(), bS.getWidth(), bS.getHeight(), bgCol, bgCol, Corners::objectCornerRadius); auto const rms = Decibels::decibelsToGain(values[1] - 10.0f); auto const peak = Decibels::decibelsToGain(values[0] - 10.0f); @@ -224,40 +226,40 @@ class VUMeterObject final : public ObjectBase { auto getColourForLevel = [](float const level) { if (level < -12) { - return nvgRGBA(66, 163, 198, 255); + return nanovg::nvgRGBA(66, 163, 198, 255); } if (level > 0) { - return nvgRGBA(255, 0, 0, 255); + return nanovg::nvgRGBA(255, 0, 0, 255); } - return nvgRGBA(255, 127, 0, 255); + return nanovg::nvgRGBA(255, 127, 0, 255); }; NVGcolor const peakColour = getColourForLevel(values[0]); NVGcolor const barColour = getColourForLevel(values[1]); // VU Bar - nvgFillColor(nvg, barColour); - nvgBeginPath(nvg); - nvgRoundedRectVarying(nvg, 4, getHeight() - barLength, getWidth() - 8, barLength, 0.0f, 0.0f, Corners::objectCornerRadius, Corners::objectCornerRadius); - nvgFill(nvg); + nanovg::nvgFillColor(nvg, barColour); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRoundedRectVarying(nvg, 4, getHeight() - barLength, getWidth() - 8, barLength, 0.0f, 0.0f, Corners::objectCornerRadius, Corners::objectCornerRadius); + nanovg::nvgFill(nvg); - nvgBeginPath(nvg); + nanovg::nvgBeginPath(nvg); int const increment = getHeight() / 30; for (int i = 0; i < 30; i++) { - nvgMoveTo(nvg, 0, i * increment + 3); - nvgLineTo(nvg, getWidth(), i * increment + 3); + nanovg::nvgMoveTo(nvg, 0, i * increment + 3); + nanovg::nvgLineTo(nvg, getWidth(), i * increment + 3); } - nvgStrokeWidth(nvg, 1.0f); - nvgStrokeColor(nvg, bgCol); - nvgStroke(nvg); + nanovg::nvgStrokeWidth(nvg, 1.0f); + nanovg::nvgStrokeColor(nvg, bgCol); + nanovg::nvgStroke(nvg); // Peak - nvgFillColor(nvg, peakColour); - nvgFillRect(nvg, 0, getHeight() - peakPosition - 5.0f, getWidth(), 5.0f); + nanovg::nvgFillColor(nvg, peakColour); + nanovg::nvgFillRect(nvg, 0, getHeight() - peakPosition - 5.0f, getWidth(), 5.0f); // Object outline - nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nvgRGBA(0, 0, 0, 0), nvgColour(object->isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, b.getX(), b.getY(), b.getWidth(), b.getHeight(), nanovg::nvgRGBA(0, 0, 0, 0), nvgColour(object->isSelected() ? PlugDataColours::objectSelectedOutlineColour : PlugDataColours::objectOutlineColour), Corners::objectCornerRadius); } void receiveObjectMessage(hash32 const symbol, SmallArray const& atoms) override diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 7a28af226d..27e8dad915 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -27,7 +27,6 @@ #include "Canvas.h" #include "Connection.h" #include "Components/ConnectionMessageDisplay.h" -#include "Components/ConsoleMessageDisplay.h" #include "Dialogs/Dialogs.h" #include "Statusbar.h" #include "Toolbar.h" @@ -35,8 +34,8 @@ #include "Sidebar/Sidebar.h" #include "Object.h" #include "PluginMode.h" -#include "Components/TouchSelectionHelper.h" #include "NVGSurface.h" +#include "Utility/NVGGraphicsContext.h" #include "Sidebar/Console.h" #include "Sidebar/Inspector.h" @@ -53,7 +52,7 @@ void runTests(PluginEditor* editor); #include using namespace juce::gl; -#include +#include class CalloutArea final : public Component , public Timer { @@ -113,13 +112,13 @@ PluginEditor::PluginEditor(PluginProcessor& p) return true; }) , tabComponent(this) , pluginMode(nullptr) - , consoleMessageDisplay(std::make_unique(this)) - , touchSelectionHelper(std::make_unique(this)) , recentlyOpenedPanelSelector(Icons::Home, "Home") , libraryPanelSelector(Icons::ItemGrid, "Library") { keyboardLayout = OSUtils::getKeyboardLayout(); + setCachedComponentImage(new NVGSurface::InvalidationListener(nvgSurface, this)); + #if !JUCE_IOS // if we are inside a DAW / host set up the border resizer now if (!ProjectInfo::isStandalone) { @@ -317,21 +316,52 @@ PluginEditor::PluginEditor(PluginProcessor& p) editorIndex = ProjectInfo::isStandalone ? numEditors++ : 0; - if (SettingsFile::getInstance()->isUsingTouchMode()) { - addAndMakeVisible(touchSelectionHelper.get()); - } - touchSelectionHelper->setAlwaysOnTop(true); - statusbar->setAlwaysOnTop(true); addAndMakeVisible(statusbar.get()); - consoleMessageDisplay->setAlwaysOnTop(true); - addChildComponent(consoleMessageDisplay.get()); - audioToolbar = std::make_unique(pd, this); audioToolbar->setAlwaysOnTop(true); addAndMakeVisible(audioToolbar.get()); +#if JUCE_WINDOWS || JUCE_LINUX || JUCE_BSD + if (ProjectInfo::isStandalone) { + standaloneWindowMinimiseButton.reset(getLookAndFeel().createDocumentWindowButton(DocumentWindow::minimiseButton)); + standaloneWindowMaximiseButton.reset(getLookAndFeel().createDocumentWindowButton(DocumentWindow::maximiseButton)); + standaloneWindowCloseButton.reset(getLookAndFeel().createDocumentWindowButton(DocumentWindow::closeButton)); + + if (standaloneWindowMinimiseButton) { + standaloneWindowMinimiseButton->onClick = [this] { + if (auto* window = findParentComponentOfClass()) + window->minimiseButtonPressed(); + }; + } + + if (standaloneWindowMaximiseButton) { + standaloneWindowMaximiseButton->onClick = [this] { + if (auto* window = findParentComponentOfClass()) + window->maximiseButtonPressed(); + }; + } + + if (standaloneWindowCloseButton) { + standaloneWindowCloseButton->addShortcut(KeyPress(KeyPress::F4Key, ModifierKeys::altModifier, 0)); + standaloneWindowCloseButton->onClick = [this] { + if (auto* window = findParentComponentOfClass()) + window->closeButtonPressed(); + }; + } + + for (auto* button : { standaloneWindowMinimiseButton.get(), standaloneWindowMaximiseButton.get(), standaloneWindowCloseButton.get() }) { + if (button) { + button->setAlwaysOnTop(true); + button->setWantsKeyboardFocus(false); + button->setVisible(false); + addChildComponent(button); + } + } + } +#endif + for (auto* button : SmallArray { &mainMenuButton, &undoButton, @@ -443,6 +473,41 @@ void PluginEditor::setUseBorderResizer(bool const shouldUse) } } +void PluginEditor::setStandaloneWindowControlsEnabled(bool const shouldBeEnabled) +{ + standaloneWindowControlsEnabled = shouldBeEnabled; + updateStandaloneWindowControls(); +} + +void PluginEditor::updateStandaloneWindowControls() +{ +#if JUCE_WINDOWS || JUCE_LINUX || JUCE_BSD + auto* window = ProjectInfo::isStandalone ? dynamic_cast(getTopLevelComponent()) : nullptr; + auto const visible = window && !window->useNativeTitlebar() && !isInPluginMode(); + auto const enabled = visible && standaloneWindowControlsEnabled && window->isActiveWindow(); + + for (auto* button : { standaloneWindowMinimiseButton.get(), standaloneWindowMaximiseButton.get(), standaloneWindowCloseButton.get() }) { + if (button) { + button->setVisible(visible); + button->setEnabled(enabled); + + if (visible) + button->toFront(false); + } + } + + if (standaloneWindowMaximiseButton && window) + standaloneWindowMaximiseButton->setToggleState(window->isMaximised(), dontSendNotification); + + if (visible) { + auto const titleBarArea = Rectangle(0, 7, getWidth() - 6, 23); + getLookAndFeel().positionDocumentWindowButtons(*window, titleBarArea.getX(), titleBarArea.getY(), titleBarArea.getWidth(), titleBarArea.getHeight(), standaloneWindowMinimiseButton.get(), standaloneWindowMaximiseButton.get(), standaloneWindowCloseButton.get(), false); + } +#else + ignoreUnused(standaloneWindowControlsEnabled); +#endif +} + void PluginEditor::paint(Graphics& g) { auto baseColour = PlugDataColours::toolbarBackgroundColour; @@ -451,7 +516,7 @@ void PluginEditor::paint(Graphics& g) baseColour = baseColour.brighter(baseColour.getBrightness() / 2.5f); } -#if JUCE_MAC || JUCE_LINUX || JUCE_BSD +#if JUCE_LINUX || JUCE_BSD if (wantsRoundedCorners()) { g.setColour(baseColour); g.fillRoundedRectangle(getLocalBounds().toFloat(), Corners::windowCornerRadius); @@ -461,9 +526,6 @@ void PluginEditor::paint(Graphics& g) #else g.fillAll(baseColour); #endif - - // Update dialog background visibility, synced with repaint for smoothness - nvgSurface.updateWindowContextVisibility(); } // Paint file drop outline @@ -477,7 +539,7 @@ void PluginEditor::paintOverChildren(Graphics& g) auto const tabbarDepth = welcomePanelVisible ? toolbarHeight + 5.5f : toolbarHeight + 30.0f; auto const leftSidebarCollapsed = leftSidebar && leftSidebar->isVisible() && leftSidebar->isHidden(); auto const rightSidebarCollapsed = rightSidebar && rightSidebar->isVisible() && rightSidebar->isHidden(); - auto const drawToolbarOutlineAcrossDockedSidebars = !usesFloatingPanels() && (leftSidebarCollapsed || rightSidebarCollapsed); + auto const drawToolbarOutlineAcrossDockedSidebars = leftSidebarCollapsed || rightSidebarCollapsed; auto const sidebarLeft = drawToolbarOutlineAcrossDockedSidebars && rightSidebarCollapsed ? static_cast(rightSidebar->getX()) : (rightSidebar && rightSidebar->isVisible()) ? rightSidebar->getX() + 1.0f : static_cast(getWidth()); auto const sidebarRight = drawToolbarOutlineAcrossDockedSidebars && leftSidebarCollapsed ? static_cast(leftSidebar->getRight()) : (leftSidebar && leftSidebar->isVisible()) ? leftSidebar->getWidth() + 1.0f : 0.0f; @@ -493,14 +555,10 @@ void PluginEditor::paintOverChildren(Graphics& g) void PluginEditor::renderArea(NVGcontext* nvg, Rectangle const area) { - auto renderScale = nvgSurface.getRenderScale(); - if (!nvgCtx || nvgCtx->getContext() != nvg || !approximatelyEqual(nvgCtx->getPhysicalPixelScaleFactor(), renderScale)) { - nvgCtx = std::make_unique(nvg); - nvgCtx->setPhysicalPixelScaleFactor(renderScale); - } + getOrCreateNanoLLGC(nvg, nvgSurface.getRenderScale()); if (isInPluginMode()) { - nvgDrawRoundedRect(nvg, 0, -Corners::windowCornerRadius, getWidth(), getHeight() + Corners::windowCornerRadius, nvgColour(PlugDataColours::canvasBackgroundColour), nvgColour(PlugDataColours::canvasBackgroundColour), Corners::windowCornerRadius); + nanovg::nvgDrawRoundedRect(nvg, 0, -Corners::windowCornerRadius, getWidth(), getHeight() + Corners::windowCornerRadius, nvgColour(PlugDataColours::canvasBackgroundColour), nvgColour(PlugDataColours::canvasBackgroundColour), Corners::windowCornerRadius); pluginMode->render(nvg, area); } else { @@ -508,42 +566,29 @@ void PluginEditor::renderArea(NVGcontext* nvg, Rectangle const area) NVGScopedState scopedState(nvg); welcomePanel->render(nvg); } else { + NVGScopedState scopedState(nvg); tabComponent.renderArea(nvg, area); - - Graphics g(*getNanoLLGC()); - if(usesFloatingPanels()) { - if (leftSidebar->isHidden()) - leftSidebar->renderButtonsOnCanvas(nvg); - if (rightSidebar->isHidden()) - rightSidebar->renderButtonsOnCanvas(nvg); - - NVGScopedState scopedState(nvg); - nvgTranslate(nvg, statusbar->getX() - nvgSurface.getX(), statusbar->getY() - nvgSurface.getY()); - statusbar->paintEntireComponent(g, false); - } - - if (usesFloatingPanels() && touchSelectionHelper && touchSelectionHelper->getParentComponent() && touchSelectionHelper->isVisible() && area.intersects(touchSelectionHelper->getBounds() - nvgSurface.getPosition())) { - NVGScopedState scopedState(nvg); - nvgTranslate(nvg, touchSelectionHelper->getX() - nvgSurface.getX(), touchSelectionHelper->getY() - nvgSurface.getY()); - touchSelectionHelper->paintEntireComponent(g, false); - } - - if (consoleMessageDisplay->isVisible()) { - NVGScopedState scopedState(nvg); - nvgTranslate(nvg, consoleMessageDisplay->getX() - nvgSurface.getX(), consoleMessageDisplay->getY() - nvgSurface.getY()); - consoleMessageDisplay->paintEntireComponent(g, false); - } } } if (isDraggingFile) { auto toolbarHeight = welcomePanel->isVisible() ? 42 : 67; - nvgBeginPath(nvg); - nvgRoundedRect(nvg, 1, -toolbarHeight, getWidth() - 2, getHeight() + 3, Corners::windowCornerRadius - 3); - nvgStrokeColor(nvg, nvgColour(PlugDataColours::dataColour)); - nvgStrokeWidth(nvg, 2.0f); - nvgStroke(nvg); + nanovg::nvgBeginPath(nvg); + nanovg::nvgRoundedRect(nvg, 1, -toolbarHeight, getWidth() - 2, getHeight() + 3, Corners::windowCornerRadius - 3); + nanovg::nvgStrokeColor(nvg, nvgColour(PlugDataColours::dataColour)); + nanovg::nvgStrokeWidth(nvg, 2.0f); + nanovg::nvgStroke(nvg); + } +} + +NVGGraphicsContext& PluginEditor::getOrCreateNanoLLGC(NVGcontext* nvg, float const renderScale) +{ + if (!nvgCtx || nvgCtx->getContext() != nvg || !approximatelyEqual(nvgCtx->getPhysicalPixelScaleFactor(), renderScale)) { + nvgCtx = std::make_unique(nvg); + nvgCtx->setPhysicalPixelScaleFactor(renderScale); } + + return *nvgCtx; } CallOutBox& PluginEditor::showCalloutBox(std::unique_ptr content, Rectangle const screenBounds) @@ -621,7 +666,7 @@ void PluginEditor::resized() if (!alreadyResized) { ScopedValueSetter recursionBlock(alreadyResized, true); - auto totalArea = Desktop::getInstance().getDisplays().getPrimaryDisplay()->totalArea; + auto totalArea = Desktop::getInstance().getDisplays().getPrimaryDisplay()->logicalBounds.getSmallestIntegerContainer(); totalArea = OSUtils::getSafeAreaInsets().subtractedFrom(totalArea); setBounds(totalArea); window->setFullScreen(true); @@ -633,22 +678,11 @@ void PluginEditor::resized() pd->lastUIHeight = getHeight(); if (isInPluginMode()) { -#if JUCE_LINUX || JUCE_BSD - nvgSurface.setRoundedBottomCorners(true, true); -#endif - nvgSurface.updateBounds(getLocalBounds().withTrimmedTop(pluginMode->isWindowFullscreen() ? 0 : 40)); + nvgSurface.updateBounds(getLocalBounds()); + updateStandaloneWindowControls(); return; } - bool const floatingPanels = usesFloatingPanels(); - -#if JUCE_LINUX || JUCE_BSD - auto roundedLeft = welcomePanel->isVisible() || (floatingPanels && (leftSidebar->isHidden() || !leftSidebar->hasAnyPanel())); - auto roundedRight = welcomePanel->isVisible() || (floatingPanels && (rightSidebar->isHidden() || !rightSidebar->hasAnyPanel())); - nvgSurface.setRoundedBottomCorners(roundedLeft, roundedRight); -#endif - - bool const touchMode = SettingsFile::getInstance()->isUsingTouchMode(); auto const leftHasSelectors = leftSidebar && leftSidebar->isVisible() && leftSidebar->hasAnyPanel(); auto const rightHasSelectors = rightSidebar && rightSidebar->isVisible() && rightSidebar->hasAnyPanel(); auto const leftExpanded = leftHasSelectors && !leftSidebar->isHidden(); @@ -657,34 +691,7 @@ void PluginEditor::resized() auto bounds = getLocalBounds(); bounds.removeFromTop(toolbarHeight); - if (floatingPanels) { - auto statusbarBounds = getLocalBounds().removeFromBottom(46).translated(0, -10); - - if (leftHasSelectors) { - if (leftExpanded) - leftSidebar->setBounds(bounds.removeFromLeft(leftSidebar->getWidth())); - else - leftSidebar->setBounds(0, toolbarHeight, 48, bounds.getHeight()); - } - if (rightHasSelectors) { - if (rightExpanded) - rightSidebar->setBounds(bounds.removeFromRight(rightSidebar->getWidth())); - else - rightSidebar->setBounds(getWidth() - 48, toolbarHeight, 48, bounds.getHeight()); - } - - if (touchMode) { - if(leftExpanded) statusbarBounds = statusbarBounds.withX(leftSidebar->getRight()); - if(rightExpanded) statusbarBounds = statusbarBounds.withRight(rightSidebar->getX()); - touchSelectionHelper->setBounds(statusbarBounds.withSizeKeepingCentre(192, 46)); - statusbar->setBounds(statusbarBounds.removeFromLeft(208).translated(leftSidebar->getX() + 4, 0)); - } else { - statusbar->setBounds(statusbarBounds.withSizeKeepingCentre(204, 46)); - } - consoleMessageDisplay->setBounds(statusbarBounds.removeFromRight(consoleMessageDisplay->getDesiredWidth() + 2).translated(-8, 0)); - } else { - auto statusbarBounds = bounds.removeFromBottom(welcomePanel->isVisible() ? 0 : Statusbar::statusbarHeight); - + { if (leftHasSelectors) { if (leftExpanded) { leftSidebar->setBounds(bounds.removeFromLeft(leftSidebar->getWidth())); @@ -702,14 +709,11 @@ void PluginEditor::resized() } } - if (touchMode) - touchSelectionHelper->setBounds(statusbarBounds.withSizeKeepingCentre(208, Statusbar::statusbarHeight)); - statusbar->setBounds(statusbarBounds); - consoleMessageDisplay->setBounds(statusbarBounds.removeFromRight(consoleMessageDisplay->getDesiredWidth() + 2).translated(-8, 0)); + statusbar->setBounds(bounds.removeFromBottom(Statusbar::statusbarHeight)); } - nvgSurface.updateBounds(welcomePanel->isVisible() ? bounds.withTrimmedTop(6) : bounds.withTrimmedTop(31)); - welcomePanel->setBounds(bounds); + nvgSurface.updateBounds(getLocalBounds()); + welcomePanel->setBounds(bounds.withTrimmedTop(6)); tabComponent.setBounds(bounds); #if JUCE_MAC @@ -725,7 +729,7 @@ void PluginEditor::resized() offset = standalone->isFullScreen() ? 20 : offset; #endif - constexpr auto buttonDistance = 46; + constexpr auto buttonDistance = 38; auto const buttonSize = toolbarHeight + 5; mainMenuButton.setBounds(offset, 0, buttonSize, buttonSize); undoButton.setBounds(buttonDistance + offset, 0, buttonSize, buttonSize); @@ -759,6 +763,7 @@ void PluginEditor::resized() welcomePanelSearchButton.setBounds(sidebarToggleButton.getX() - buttonSize - 2, 0, buttonSize, buttonSize); welcomePanelSearchInput.setBounds(libraryPanelSelector.getRight() + 10, 4, welcomePanelSearchButton.getX() - libraryPanelSelector.getRight() - 20, toolbarHeight - 4); + updateStandaloneWindowControls(); repaint(); // Some outlines are dependent on whether or not the sidebars are expanded, or whether or not a patch is opened } @@ -767,11 +772,6 @@ bool PluginEditor::isInPluginMode() const return static_cast(pluginMode); } -bool PluginEditor::usesFloatingPanels() const -{ - return SettingsFile::getInstance()->getProperty("floating_panels"); -} - Canvas* PluginEditor::getPluginModeCanvas() const { if (isInPluginMode()) @@ -785,10 +785,10 @@ void PluginEditor::parentSizeChanged() if (!ProjectInfo::isStandalone) return; +#if JUCE_MAC auto* standalone = dynamic_cast(getTopLevelComponent()); // Hide TitleBar Buttons in Plugin Mode bool visible = !isInPluginMode(); -#if JUCE_MAC if (!standalone->useNativeTitlebar() && !visible && !standalone->isFullScreen()) { // Hide TitleBar Buttons in Plugin Mode if using native title bar if (ComponentPeer* peer = standalone->getPeer()) @@ -799,12 +799,7 @@ void PluginEditor::parentSizeChanged() OSUtils::hideTitlebarButtons(peer, false, false, false); } #else - if (!standalone->useNativeTitlebar()) { - // Hide/Show TitleBar Buttons in Plugin Mode - standalone->getCloseButton()->setVisible(visible); - standalone->getMinimiseButton()->setVisible(visible); - standalone->getMaximiseButton()->setVisible(visible); - } + updateStandaloneWindowControls(); #endif resized(); @@ -878,7 +873,7 @@ void PluginEditor::mouseDown(MouseEvent const& e) getPeer()->setBounds(unmaximisedSize, false); } else { unmaximisedSize = getTopLevelComponent()->getBounds(); - auto const userArea = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea; + auto const userArea = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userBounds.getSmallestIntegerContainer(); getPeer()->setBounds(userArea, false); } @@ -1061,8 +1056,6 @@ void PluginEditor::updateConsole(SmallString const& message, bool messageIsWarni { if (auto* s = getSidebarForPanel(Sidebar::ConsolePanel)) { s->updateConsole(numMessages, newWarning); - if (s->isHidden()) - consoleMessageDisplay->showMessage(message, messageIsWarning); } } @@ -1104,19 +1097,9 @@ void PluginEditor::valueChanged(Value& v) void PluginEditor::settingsChanged(String const& name, var const& value) { if (name == "touch_mode") { - if (static_cast(value)) { - addChildComponent(touchSelectionHelper.get()); - } else { - removeChildComponent(touchSelectionHelper.get()); - } + // The statusbar owns the touch selection helper; refresh its visibility triggerAsyncUpdate(); resized(); - } else if (name == "floating_panels") { - if (leftSidebar) - leftSidebar->resized(); - if (rightSidebar) - rightSidebar->resized(); - resized(); } else if (name == "last_welcome_panel") { if(static_cast(value) == 0) @@ -1139,6 +1122,8 @@ void PluginEditor::handleAsyncUpdate() { tabComponent.repaint(); // So tab dirty titles can be reflected + bool const touchMode = SettingsFile::getInstance()->isUsingTouchMode(); + if (auto const* cnv = getCurrentCanvas()) { bool locked = getValue(cnv->locked); bool const commandLocked = getValue(cnv->commandLocked); @@ -1162,22 +1147,13 @@ void PluginEditor::handleAsyncUpdate() commandManager.commandStatusChanged(); addObjectMenuButton.setEnabled(true); - if (touchSelectionHelper) { - if (!locked) { - touchSelectionHelper->show(); - } else { - touchSelectionHelper->setVisible(false); - } - } + statusbar->showTouchSelectionHelper(touchMode && !locked); } else { undoButton.setEnabled(false); redoButton.setEnabled(false); addObjectMenuButton.setEnabled(false); - if (touchSelectionHelper) - touchSelectionHelper->setVisible(false); + statusbar->showTouchSelectionHelper(false); } - if (touchSelectionHelper) - touchSelectionHelper->repaint(); } void PluginEditor::updateSelection(Canvas* cnv) @@ -1994,8 +1970,8 @@ bool PluginEditor::perform(InvocationInfo const& info) auto outobj = cnv->getSelectionOfType().front()->outobj; auto pos = Point( // place beneath outlet object + Object::margin - cnv->lastSelectedConnection->getX() + cnv->lastSelectedConnection->getWidth() / 2 - 12, - cnv->lastSelectedConnection->getY() + cnv->lastSelectedConnection->getHeight() / 2 - 12); + cnv->lastSelectedConnection->getX() + cnv->lastSelectedConnection->Component::getWidth() / 2 - 12, + cnv->lastSelectedConnection->getY() + cnv->lastSelectedConnection->Component::getHeight() / 2 - 12); cnv->objects.add(cnv, objectNames.at(ID), pos); cnv->patch.endUndoSequence("ObjectInConnection"); } else { @@ -2173,7 +2149,16 @@ Object* PluginEditor::highlightSearchTarget(void* target, bool const openNewTabI return nullptr; } - auto calculateDimensionsAndProcess = [cnv, found, viewport] { + auto calculateDimensionsAndProcess = [_cnv = Component::SafePointer(cnv), _found = Component::SafePointer(found)] { + auto* cnv = _cnv.getComponent(); + auto* found = _found.getComponent(); + if (!cnv || !found) + return; + + auto* viewport = cnv->viewport.get(); + if (!viewport) + return; + auto const scale = getValue(cnv->zoomScale); if (!viewport->getBoundsInParent().contains(found->getBounds())) { @@ -2203,9 +2188,6 @@ Object* PluginEditor::highlightSearchTarget(void* target, bool const openNewTabI } }; - // FIXME: We have to wait for the viewport to call internally resized before the size is correct! - // So we check if the bounds are 0,0,0,0, and if so, post the calculations to the message thread - // which _should_ place this event after the resize 🙏 if (viewport->getBoundsInParent().isEmpty()) MessageManager::callAsync(calculateDimensionsAndProcess); else diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index bd4508c928..12494523ab 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -34,11 +34,9 @@ class TabComponent; class PluginProcessor; class Autosave; class PluginMode; -class TouchSelectionHelper; class WelcomePanel; class CalloutArea; class NVGGraphicsContext; -class ConsoleMessageDisplay; class Console; class DocumentationBrowser; class AutomationPanel; @@ -123,7 +121,6 @@ class PluginEditor final : public AudioProcessorEditor bool perform(InvocationInfo const& info) override; bool wantsRoundedCorners() const; - bool usesFloatingPanels() const; bool keyPressed(KeyPress const& key) override; @@ -133,6 +130,8 @@ class PluginEditor final : public AudioProcessorEditor void commandKeyChanged(bool isHeld) override; void setUseBorderResizer(bool shouldUse); + void updateStandaloneWindowControls(); + void setStandaloneWindowControlsEnabled(bool shouldBeEnabled); Sidebar* getLeftSidebar() const { return leftSidebar.get(); } Sidebar* getRightSidebar() const { return rightSidebar.get(); } @@ -195,16 +194,15 @@ class PluginEditor final : public AudioProcessorEditor return nvgCtx.get(); } + NVGGraphicsContext& getOrCreateNanoLLGC(NVGcontext* nvg, float renderScale); + private: TabComponent tabComponent; public: std::unique_ptr pluginMode; - std::unique_ptr consoleMessageDisplay; private: - std::unique_ptr touchSelectionHelper; - // Used by standalone to handle dragging the window WindowDragger windowDragger; @@ -232,6 +230,11 @@ class PluginEditor final : public AudioProcessorEditor // Used in standalone std::unique_ptr> borderResizer; + std::unique_ptr