diff --git a/GraphicApplication/CMakeLists.txt b/GraphicApplication/CMakeLists.txt index ccfd36a4..640563c3 100644 --- a/GraphicApplication/CMakeLists.txt +++ b/GraphicApplication/CMakeLists.txt @@ -17,6 +17,10 @@ endfunction() graphic_application_library(${PROJECT_NAME}InputLayer input_layer) target_link_libraries(${PROJECT_NAME}InputLayer PUBLIC Math Modules) +### ---------------------- Window vocabulary (cursor, clipboard, platform facts) --------------------- ### +graphic_application_library(${PROJECT_NAME}WindowTypes window_types) +target_link_libraries(${PROJECT_NAME}WindowTypes PUBLIC Math) + ### ---------------------- Diagnostics --------------------- ### graphic_application_library(${PROJECT_NAME}Diagnostics diagnostics) target_link_libraries(${PROJECT_NAME}Diagnostics PUBLIC Callstack Threads::Threads) @@ -71,7 +75,7 @@ endif () ### ---------------------- Backend contract --------------------- ### graphic_application_library(${PROJECT_NAME}BackendContract backends/contract) -target_link_libraries(${PROJECT_NAME}BackendContract PUBLIC Math ${PROJECT_NAME}InputLayer ${PROJECT_NAME}Gpu) +target_link_libraries(${PROJECT_NAME}BackendContract PUBLIC Math ${PROJECT_NAME}InputLayer ${PROJECT_NAME}WindowTypes ${PROJECT_NAME}Gpu) ### ---------------------- Platform backend --------------------- ### if (ANDROID) @@ -87,13 +91,25 @@ target_link_libraries(${PROJECT_NAME}Backend PUBLIC ${PROJECT_NAME}BackendContra graphic_application_library(${PROJECT_NAME}Window window) target_link_libraries(${PROJECT_NAME}Window PUBLIC Math ${PROJECT_NAME}BackendContract PRIVATE ${PROJECT_NAME}Backend) -### ---------------------- Canvas --------------------- ### +### ---------------------- Canvas contract (what a canvas backend implements) --------------------- ### +graphic_application_library(${PROJECT_NAME}CanvasContract canvas/backends/contract) +target_link_libraries(${PROJECT_NAME}CanvasContract PUBLIC Math) + +### ---------------------- Canvas (portable drawing facade) --------------------- ### graphic_application_library(${PROJECT_NAME}Canvas canvas) -target_link_libraries(${PROJECT_NAME}Canvas PUBLIC Math Containers ${PROJECT_NAME}BackendContract PRIVATE ${PROJECT_NAME}Diagnostics NanovgVk) +target_link_libraries(${PROJECT_NAME}Canvas PUBLIC ${PROJECT_NAME}CanvasContract Containers) + +### ---------------------- Recording canvas backend (headless tests) --------------------- ### +graphic_application_library(${PROJECT_NAME}CanvasRecording canvas/backends/recording) +target_link_libraries(${PROJECT_NAME}CanvasRecording PUBLIC ${PROJECT_NAME}CanvasContract) + +### ---------------------- NanoVG canvas backend (window surfaces) --------------------- ### +graphic_application_library(${PROJECT_NAME}CanvasNanoVg canvas/backends/nanovg) +target_link_libraries(${PROJECT_NAME}CanvasNanoVg PUBLIC ${PROJECT_NAME}Canvas ${PROJECT_NAME}BackendContract PRIVATE ${PROJECT_NAME}Diagnostics NanovgVk) ### ---------------------- Window decorations --------------------- ### graphic_application_library(${PROJECT_NAME}Decorations decorations) -target_link_libraries(${PROJECT_NAME}Decorations PUBLIC ${PROJECT_NAME}Canvas) +target_link_libraries(${PROJECT_NAME}Decorations PUBLIC ${PROJECT_NAME}Canvas ${PROJECT_NAME}BackendContract) ### ---------------------- Debug UI --------------------- ### graphic_application_library(${PROJECT_NAME}DebugUI debug_ui) @@ -101,7 +117,7 @@ target_link_libraries(${PROJECT_NAME}DebugUI PUBLIC ${PROJECT_NAME}Window PRIVAT ### ---------------------- Application --------------------- ### graphic_application_library(${PROJECT_NAME} application) -target_link_libraries(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}Window ${PROJECT_NAME}Canvas ${PROJECT_NAME}Decorations ${PROJECT_NAME}DebugUI ${PROJECT_NAME}Diagnostics) +target_link_libraries(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}Window ${PROJECT_NAME}CanvasNanoVg ${PROJECT_NAME}Decorations ${PROJECT_NAME}DebugUI ${PROJECT_NAME}Diagnostics) target_compile_definitions(${PROJECT_NAME} PRIVATE MODULES_GENERIC_APP_ID="online.ilushabasic.modules" MODULES_GENERIC_APP_NAME="Modules") # Copies the shared fonts into an "rsc" directory next to an executable, where the desktop backend looks for them. @@ -126,8 +142,8 @@ endif () if (NOT ANDROID) file(GLOB TEST_SOURCES CONFIGURE_DEPENDS "./tests/*.cpp") add_executable(Test${PROJECT_NAME} ${TEST_SOURCES}) - target_include_directories(Test${PROJECT_NAME} PRIVATE ./src/canvas/private ./src/backends/wayland/private) + target_include_directories(Test${PROJECT_NAME} PRIVATE ./src/canvas/backends/nanovg/private ./src/backends/wayland/private) target_compile_definitions(Test${PROJECT_NAME} PRIVATE MODULES_SOURCE_DIR="${CMAKE_SOURCE_DIR}") - target_link_libraries(Test${PROJECT_NAME} ${PROJECT_NAME}Window ${PROJECT_NAME}Backend ${PROJECT_NAME}Diagnostics NanovgVk UnitTest++) + target_link_libraries(Test${PROJECT_NAME} ${PROJECT_NAME}Window ${PROJECT_NAME}Backend ${PROJECT_NAME}Diagnostics ${PROJECT_NAME}Canvas ${PROJECT_NAME}CanvasRecording NanovgVk UnitTest++) add_test(NAME Test${PROJECT_NAME} COMMAND Test${PROJECT_NAME}) endif () diff --git a/GraphicApplication/src/application/private/GraphicApplication.cpp b/GraphicApplication/src/application/private/GraphicApplication.cpp index 30df86be..cd344bcb 100644 --- a/GraphicApplication/src/application/private/GraphicApplication.cpp +++ b/GraphicApplication/src/application/private/GraphicApplication.cpp @@ -132,13 +132,13 @@ void Application::present() { return; } - mCanvas->drawBegin(*frame, mWindow->frame()); + mCanvas->beginFrame(*frame, mWindow->frame()); mDebugGUI->drawBegin(); drawFrameContent(); enterPhase(UiPhase::Present); - mCanvas->drawEnd(); + mCanvas->endFrame(); mDebugGUI->drawEnd(*frame); mWindow->loop().frameEnd(); @@ -149,12 +149,13 @@ void Application::drawFrameContent() { const ProfileScope scope(mProfiler, mDrawSection); const WindowFrame& frame = mWindow->frame(); - mDecorations.paintBase(*mCanvas, frame); + Canvas& canvas = *mCanvas; + mDecorations.paintBase(canvas, frame); { - const Canvas::SavedState saved(*mCanvas); - mDelegate.drawFrame(*mCanvas); + const Canvas::SavedState saved(canvas); + mDelegate.drawFrame(canvas); } - mDecorations.paintFrame(*mCanvas, frame, mWindow->controls().isFocused()); + mDecorations.paintFrame(canvas, frame, mWindow->controls().isFocused()); } void Application::updateFrameCounters() { diff --git a/GraphicApplication/src/backends/contract/private/WindowFrame.cpp b/GraphicApplication/src/backends/contract/private/WindowFrame.cpp index 584649ad..2243e6b0 100644 --- a/GraphicApplication/src/backends/contract/private/WindowFrame.cpp +++ b/GraphicApplication/src/backends/contract/private/WindowFrame.cpp @@ -4,21 +4,6 @@ using namespace tp; -CursorShape tp::cursorForEdge(WindowEdge edge) { - switch (edge) { - case WindowEdge::Left: - case WindowEdge::Right: return CursorShape::ResizeHorizontal; - case WindowEdge::Top: - case WindowEdge::Bottom: return CursorShape::ResizeVertical; - case WindowEdge::TopLeft: - case WindowEdge::BottomRight: return CursorShape::ResizeNorthWestSouthEast; - case WindowEdge::TopRight: - case WindowEdge::BottomLeft: return CursorShape::ResizeNorthEastSouthWest; - case WindowEdge::None: return CursorShape::Default; - } - return CursorShape::Default; -} - Vec2F WindowFrame::surfaceSize() const { const halnf inset = margin(); return { mContentSize.x + inset * 2, mContentSize.y + inset * 2 }; diff --git a/GraphicApplication/src/canvas/backends/contract/public/CanvasBackend.hpp b/GraphicApplication/src/canvas/backends/contract/public/CanvasBackend.hpp new file mode 100644 index 00000000..68871508 --- /dev/null +++ b/GraphicApplication/src/canvas/backends/contract/public/CanvasBackend.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "CanvasTypes.hpp" + +#include +#include + +namespace tp { + + // What a canvas draws with. Every coordinate is already in surface space: the canvas has applied its origin, and the + // clip it passes is the intersection of every open clip scope. + class CanvasBackend : public TextMetrics { + public: + enum class CompositeOperation { SourceOver, DestinationOut, DestinationOver }; + enum class PathWinding { Solid, Hole }; + + struct LinearGradient { + Vec2F from; + Vec2F to; + RGBA fromColor; + RGBA toColor; + }; + + struct BoxGradient { + RectF box; + halnf radius = 0; + halnf feather = 0; + RGBA innerColor; + RGBA outerColor; + }; + + virtual void setClip(std::optional clip) = 0; + virtual void setCompositeOperation(CompositeOperation operation) = 0; + + virtual void beginPath() = 0; + virtual void addPathRect(const RectF& rect, halnf round) = 0; + virtual void setPathWinding(PathWinding winding) = 0; + virtual void fillPath(const RGBA& color) = 0; + virtual void fillPath(const BoxGradient& gradient) = 0; + + virtual void rect(const RectF& rect, const RGBA& color, halnf round) = 0; + virtual void gradientRect(const RectF& rect, const RGBA& from, const RGBA& to, Axis axis, halnf round) = 0; + virtual void outline(const RectF& rect, const RGBA& color, halnf round, halnf width) = 0; + virtual void circle(Vec2F center, halnf radius, const RGBA& color) = 0; + virtual void circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width) = 0; + virtual void line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness) = 0; + virtual void polyline(const Vec2F* points, int count, const RGBA& color, halnf width) = 0; + virtual void polygon(const Vec2F* points, int count, const RGBA& color) = 0; + virtual void polygon(const Vec2F* points, int count, const LinearGradient& gradient) = 0; + virtual void ringSegment( + Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient + ) = 0; + virtual void + radialGlow(Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor) = 0; + + virtual void text(const std::string& text, const RectF& area, const TextStyle& style) = 0; + virtual void rotatedText(const std::string& text, Vec2F center, halnf size, halnf angle, const RGBA& color) = 0; + + [[nodiscard]] virtual ImageHandle createImageFromRenderTarget(uint32_t renderTargetId) = 0; + virtual void updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId) = 0; + [[nodiscard]] virtual ImageHandle createImageFromFile(const char* path) = 0; + [[nodiscard]] virtual ImageHandle createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba) = 0; + virtual void updateImageRGBA(ImageHandle image, const unsigned char* rgba) = 0; + [[nodiscard]] virtual Vec2F imageSize(ImageHandle image) = 0; + virtual void deleteImage(ImageHandle image) = 0; + virtual void drawImage(const RectF& rect, ImageHandle image, halnf angle, halnf alpha, halnf rounding) = 0; + + protected: + [[nodiscard]] static ImageHandle makeImageHandle(int id) { return ImageHandle(id); } + }; +} diff --git a/GraphicApplication/src/canvas/backends/contract/public/CanvasTypes.hpp b/GraphicApplication/src/canvas/backends/contract/public/CanvasTypes.hpp new file mode 100644 index 00000000..74c2ffce --- /dev/null +++ b/GraphicApplication/src/canvas/backends/contract/public/CanvasTypes.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include "Color.hpp" +#include "Rect.hpp" + +#include +#include + +namespace tp { + + enum class Axis { Horizontal = 0, Vertical = 1 }; + + enum class HorizontalAlign { Left, Center, Right }; + + enum class VerticalAlign { Top, Center, Bottom }; + + struct TextAlign { + HorizontalAlign horizontal = HorizontalAlign::Left; + VerticalAlign vertical = VerticalAlign::Center; + + static constexpr TextAlign leftCenter() { return { HorizontalAlign::Left, VerticalAlign::Center }; } + static constexpr TextAlign centered() { return { HorizontalAlign::Center, VerticalAlign::Center }; } + static constexpr TextAlign rightCenter() { return { HorizontalAlign::Right, VerticalAlign::Center }; } + + [[nodiscard]] bool operator==(const TextAlign& other) const = default; + }; + + enum class FontFace { Regular, Bold, Italic, BoldItalic, Monospace }; + + struct TextStyle { + halnf size = 16; + RGBA color = RGBA(1, 1, 1, 1); + TextAlign align; + halnf padding = 0; + FontFace face = FontFace::Regular; + }; + + class ImageHandle { + public: + ImageHandle() = default; + + [[nodiscard]] bool isValid() const { return mId > 0; } + [[nodiscard]] int id() const { return mId; } + [[nodiscard]] bool operator==(const ImageHandle& other) const = default; + + private: + friend class Canvas; + friend class CanvasBackend; + + explicit ImageHandle(int id) : + mId(id) {} + + int mId = 0; + }; + + // Measures text the way a canvas draws it; layout uses it without a GPU frame. + class TextMetrics { + public: + virtual ~TextMetrics() = default; + + [[nodiscard]] virtual halnf textWidth(const std::string& text, halnf size, FontFace face) const = 0; + + // Pen position before every byte of the text and after its end (text.size() + 1 values). + [[nodiscard]] virtual std::vector prefixWidths(const std::string& text, halnf size, FontFace face) const = 0; + }; +} diff --git a/GraphicApplication/src/canvas/private/CanvasFontLoader.cpp b/GraphicApplication/src/canvas/backends/nanovg/private/CanvasFontLoader.cpp similarity index 100% rename from GraphicApplication/src/canvas/private/CanvasFontLoader.cpp rename to GraphicApplication/src/canvas/backends/nanovg/private/CanvasFontLoader.cpp diff --git a/GraphicApplication/src/canvas/private/CanvasFontLoader.hpp b/GraphicApplication/src/canvas/backends/nanovg/private/CanvasFontLoader.hpp similarity index 100% rename from GraphicApplication/src/canvas/private/CanvasFontLoader.hpp rename to GraphicApplication/src/canvas/backends/nanovg/private/CanvasFontLoader.hpp diff --git a/GraphicApplication/src/canvas/private/CanvasFonts.hpp b/GraphicApplication/src/canvas/backends/nanovg/private/CanvasFonts.hpp similarity index 100% rename from GraphicApplication/src/canvas/private/CanvasFonts.hpp rename to GraphicApplication/src/canvas/backends/nanovg/private/CanvasFonts.hpp diff --git a/GraphicApplication/src/canvas/backends/nanovg/private/CanvasSurface.cpp b/GraphicApplication/src/canvas/backends/nanovg/private/CanvasSurface.cpp new file mode 100644 index 00000000..aa317162 --- /dev/null +++ b/GraphicApplication/src/canvas/backends/nanovg/private/CanvasSurface.cpp @@ -0,0 +1,26 @@ +#include "Canvas.hpp" + +#include "Assert.hpp" +#include "NanoVgCanvasBackend.hpp" + +using namespace tp; + +// The parts of the canvas facade that only exist on a window surface. They live here, next to the NanoVG backend, so +// the portable canvas library never names a GPU frame or a window. + +Canvas::Canvas(WindowSurface& surface, Profiler& profiler, const std::filesystem::path& resourceDirectory) { + auto backend = std::make_unique(surface, profiler, resourceDirectory); + mSurfaceBackend = backend.get(); + mBackend = std::move(backend); +} + +void Canvas::beginFrame(const GpuFrame& frame, const WindowFrame& windowFrame) { + DEBUG_ASSERT(mSurfaceBackend); + mSurfaceBackend->beginFrame(frame, windowFrame); + beginFrame(); +} + +void Canvas::endFrame() { + DEBUG_ASSERT(mSurfaceBackend); + mSurfaceBackend->endFrame(); +} diff --git a/GraphicApplication/src/canvas/backends/nanovg/private/NanoVgCanvasBackend.cpp b/GraphicApplication/src/canvas/backends/nanovg/private/NanoVgCanvasBackend.cpp new file mode 100644 index 00000000..c5722ca8 --- /dev/null +++ b/GraphicApplication/src/canvas/backends/nanovg/private/NanoVgCanvasBackend.cpp @@ -0,0 +1,407 @@ +#include "NanoVgCanvasBackend.hpp" + +#include "GpuError.hpp" +#include "RenderTargetRegistry.hpp" +#include "Profiler.hpp" + +#include + +#include + +using namespace tp; + +namespace { + constexpr halnf kItalicSkew = -0.21f; + + bool isItalic(FontFace face) { return face == FontFace::Italic || face == FontFace::BoldItalic; } + + void addRect(NVGcontext* vg, const RectF& rect, halnf round) { + if (round == 0) { + nvgRect(vg, rect.x, rect.y, rect.z, rect.w); + } else { + nvgRoundedRect(vg, rect.x, rect.y, rect.z, rect.w, round); + } + } + + void stroke(NVGcontext* vg, const RGBA& color, halnf width) { + nvgStrokeWidth(vg, width); + nvgStrokeColor(vg, toNVGColor(color)); + nvgStroke(vg); + } + + void fill(NVGcontext* vg, const RGBA& color) { + nvgFillColor(vg, toNVGColor(color)); + nvgFill(vg); + } + + void addPoints(NVGcontext* vg, const Vec2F* points, int count) { + nvgMoveTo(vg, points[0].x, points[0].y); + for (int index = 1; index < count; index++) { + nvgLineTo(vg, points[index].x, points[index].y); + } + } + + NVGpaint linearPaint(NVGcontext* vg, const CanvasBackend::LinearGradient& gradient) { + return nvgLinearGradient( + vg, + gradient.from.x, + gradient.from.y, + gradient.to.x, + gradient.to.y, + toNVGColor(gradient.fromColor), + toNVGColor(gradient.toColor) + ); + } + + int nvgCompositeOperationOf(CanvasBackend::CompositeOperation operation) { + switch (operation) { + case CanvasBackend::CompositeOperation::SourceOver: return NVG_SOURCE_OVER; + case CanvasBackend::CompositeOperation::DestinationOut: return NVG_DESTINATION_OUT; + case CanvasBackend::CompositeOperation::DestinationOver: return NVG_DESTINATION_OVER; + } + return NVG_SOURCE_OVER; + } + + int nvgWindingOf(CanvasBackend::PathWinding winding) { + return winding == CanvasBackend::PathWinding::Hole ? NVG_HOLE : NVG_SOLID; + } + + int nvgAlignmentOf(const TextAlign& align) { + int alignment = 0; + switch (align.horizontal) { + case HorizontalAlign::Left: alignment |= NVG_ALIGN_LEFT; break; + case HorizontalAlign::Center: alignment |= NVG_ALIGN_CENTER; break; + case HorizontalAlign::Right: alignment |= NVG_ALIGN_RIGHT; break; + } + switch (align.vertical) { + case VerticalAlign::Top: alignment |= NVG_ALIGN_TOP; break; + case VerticalAlign::Center: alignment |= NVG_ALIGN_MIDDLE; break; + case VerticalAlign::Bottom: alignment |= NVG_ALIGN_BOTTOM; break; + } + return alignment; + } + + Vec2F textAnchor(const RectF& rect, const TextAlign& align) { + Vec2F anchor = rect.pos; + if (align.horizontal == HorizontalAlign::Center) { + anchor.x += rect.z * 0.5f; + } + if (align.horizontal == HorizontalAlign::Right) { + anchor.x += rect.z; + } + if (align.vertical == VerticalAlign::Center) { + anchor.y += rect.w * 0.5f; + } + if (align.vertical == VerticalAlign::Bottom) { + anchor.y += rect.w; + } + return anchor; + } +} + +NanoVgCanvasBackend::NanoVgCanvasBackend( + WindowSurface& surface, Profiler& profiler, const std::filesystem::path& resourceDirectory +) : + mSurface(surface), + mProfiler(profiler), + mFlushSection(profiler.section("frame.ui_flush")) { + const GpuDevice& gpu = surface.gpu(); + VKNVGCreateInfo createInfo{}; + createInfo.gpu = gpu.physicalDevice(); + createInfo.device = gpu.device(); + createInfo.renderpass = surface.renderPass(); + createInfo.cmdBuffer = mFrameCommandBuffers; + createInfo.frameCount = WindowSurface::kFramesInFlight; + createInfo.currentFrame = &mFrameSlot; + createInfo.commandPool = gpu.commandPool(); + createInfo.graphicsQueueFamilyIndex = gpu.graphicsQueueFamily(); + + mVg = nvgCreateVk(&createInfo, NVG_ANTIALIAS | NVG_STENCIL_STROKES, gpu.graphicsQueue()); + if (!mVg) { + throw GpuError("creating the NanoVG context", VK_ERROR_INITIALIZATION_FAILED); + } + mFonts = loadCanvasFonts(mVg, resourceDirectory); +} + +NanoVgCanvasBackend::~NanoVgCanvasBackend() { + mSurface.gpu().waitIdle(); + nvgDeleteVk(mVg); +} + +void NanoVgCanvasBackend::beginFrame(const GpuFrame& frame, const WindowFrame& windowFrame) { + mFrameCommandBuffers[frame.slot] = frame.commandBuffer; + mFrameSlot = frame.slot; + + const Vec2F surfaceSize = windowFrame.surfaceSize(); + const halnf devicePixelRatio = surfaceSize.x > 0 ? (halnf) frame.extent.width / surfaceSize.x : 1.0f; + nvgBeginFrame(mVg, surfaceSize.x, surfaceSize.y, devicePixelRatio); + nvgTranslate(mVg, windowFrame.margin(), windowFrame.margin()); +} + +void NanoVgCanvasBackend::endFrame() { + const ProfileScope scope(mProfiler, mFlushSection); + nvgEndFrame(mVg); +} + +void NanoVgCanvasBackend::setClip(std::optional clip) { + if (!clip) { + nvgResetScissor(mVg); + return; + } + nvgScissor(mVg, clip->x, clip->y, clip->z, clip->w); +} + +void NanoVgCanvasBackend::setCompositeOperation(CompositeOperation operation) { + nvgGlobalCompositeOperation(mVg, nvgCompositeOperationOf(operation)); +} + +void NanoVgCanvasBackend::beginPath() { nvgBeginPath(mVg); } + +void NanoVgCanvasBackend::addPathRect(const RectF& rect, halnf round) { addRect(mVg, rect, round); } + +void NanoVgCanvasBackend::setPathWinding(PathWinding winding) { nvgPathWinding(mVg, nvgWindingOf(winding)); } + +void NanoVgCanvasBackend::fillPath(const RGBA& color) { fill(mVg, color); } + +void NanoVgCanvasBackend::fillPath(const BoxGradient& gradient) { + const RectF& box = gradient.box; + const NVGpaint paint = nvgBoxGradient( + mVg, + box.x, + box.y, + box.z, + box.w, + gradient.radius, + gradient.feather, + toNVGColor(gradient.innerColor), + toNVGColor(gradient.outerColor) + ); + nvgFillPaint(mVg, paint); + nvgFill(mVg); +} + +void NanoVgCanvasBackend::rect(const RectF& rect, const RGBA& color, halnf round) { + nvgBeginPath(mVg); + addRect(mVg, rect, round); + fill(mVg, color); +} + +void NanoVgCanvasBackend::gradientRect(const RectF& rect, const RGBA& from, const RGBA& to, Axis axis, halnf round) { + const bool horizontal = axis == Axis::Horizontal; + const halnf endX = horizontal ? rect.x + rect.z : rect.x; + const halnf endY = horizontal ? rect.y : rect.y + rect.w; + const NVGpaint paint = nvgLinearGradient(mVg, rect.x, rect.y, endX, endY, toNVGColor(from), toNVGColor(to)); + nvgBeginPath(mVg); + addRect(mVg, rect, round); + nvgFillPaint(mVg, paint); + nvgFill(mVg); +} + +void NanoVgCanvasBackend::outline(const RectF& rect, const RGBA& color, halnf round, halnf width) { + const halnf inset = width / 2; + nvgBeginPath(mVg); + nvgRoundedRect( + mVg, rect.x + inset, rect.y + inset, rect.z - width, rect.w - width, round > inset ? round - inset : 0 + ); + stroke(mVg, color, width); +} + +void NanoVgCanvasBackend::circle(Vec2F center, halnf radius, const RGBA& color) { + nvgBeginPath(mVg); + nvgCircle(mVg, center.x, center.y, radius); + fill(mVg, color); +} + +void NanoVgCanvasBackend::circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width) { + nvgBeginPath(mVg); + nvgCircle(mVg, center.x, center.y, radius - width / 2); + stroke(mVg, color, width); +} + +void NanoVgCanvasBackend::line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness) { + nvgBeginPath(mVg); + nvgMoveTo(mVg, start.x, start.y); + nvgLineTo(mVg, end.x, end.y); + stroke(mVg, color, thickness); +} + +void NanoVgCanvasBackend::polyline(const Vec2F* points, int count, const RGBA& color, halnf width) { + if (count < 2) { + return; + } + + nvgBeginPath(mVg); + addPoints(mVg, points, count); + nvgLineCap(mVg, NVG_ROUND); + nvgLineJoin(mVg, NVG_ROUND); + stroke(mVg, color, width); +} + +void NanoVgCanvasBackend::polygon(const Vec2F* points, int count, const RGBA& color) { + if (count < 3) { + return; + } + + nvgBeginPath(mVg); + addPoints(mVg, points, count); + nvgClosePath(mVg); + fill(mVg, color); +} + +void NanoVgCanvasBackend::polygon(const Vec2F* points, int count, const LinearGradient& gradient) { + if (count < 3) { + return; + } + + nvgBeginPath(mVg); + addPoints(mVg, points, count); + nvgClosePath(mVg); + nvgFillPaint(mVg, linearPaint(mVg, gradient)); + nvgFill(mVg); +} + +void NanoVgCanvasBackend::ringSegment( + Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient +) { + nvgBeginPath(mVg); + nvgArc(mVg, center.x, center.y, innerRadius, fromAngle, toAngle, NVG_CW); + nvgArc(mVg, center.x, center.y, outerRadius, toAngle, fromAngle, NVG_CCW); + nvgClosePath(mVg); + nvgFillPaint(mVg, linearPaint(mVg, gradient)); + nvgFill(mVg); +} + +void NanoVgCanvasBackend::radialGlow( + Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor +) { + const NVGpaint paint = nvgRadialGradient( + mVg, center.x, center.y, innerRadius, outerRadius, toNVGColor(innerColor), toNVGColor(outerColor) + ); + + nvgBeginPath(mVg); + nvgCircle(mVg, center.x, center.y, outerRadius); + nvgFillPaint(mVg, paint); + nvgFill(mVg); +} + +void NanoVgCanvasBackend::text(const std::string& string, const RectF& area, const TextStyle& style) { + nvgSave(mVg); + nvgFontSize(mVg, style.size); + nvgFontFaceId(mVg, mFonts.idFor(style.face)); + nvgFillColor(mVg, toNVGColor(style.color)); + nvgTextAlign(mVg, nvgAlignmentOf(style.align)); + + const Vec2F anchor = textAnchor(area, style.align); + if (isItalic(style.face)) { + nvgTranslate(mVg, anchor.x, anchor.y); + nvgSkewX(mVg, kItalicSkew); + nvgText(mVg, 0, 0, string.c_str(), nullptr); + } else { + nvgText(mVg, anchor.x, anchor.y, string.c_str(), nullptr); + } + nvgRestore(mVg); +} + +void NanoVgCanvasBackend::rotatedText( + const std::string& string, Vec2F center, halnf size, halnf angle, const RGBA& color +) { + nvgSave(mVg); + nvgTranslate(mVg, center.x, center.y); + nvgRotate(mVg, angle); + nvgFontSize(mVg, size); + nvgFontFaceId(mVg, mFonts.regular); + nvgTextAlign(mVg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); + nvgFillColor(mVg, toNVGColor(color)); + nvgText(mVg, 0, 0, string.c_str(), nullptr); + nvgRestore(mVg); +} + +halnf NanoVgCanvasBackend::textWidth(const std::string& string, halnf size, FontFace face) const { + nvgFontSize(mVg, size); + nvgFontFaceId(mVg, mFonts.idFor(face)); + return nvgTextBounds(mVg, 0, 0, string.c_str(), nullptr, nullptr); +} + +std::vector NanoVgCanvasBackend::prefixWidths(const std::string& string, halnf size, FontFace face) const { + const size_t length = string.size(); + std::vector widths(length + 1, 0); + + nvgFontSize(mVg, size); + nvgFontFaceId(mVg, mFonts.idFor(face)); + nvgTextAlign(mVg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); + + const char* start = string.c_str(); + std::vector glyphs(length); + const int count = nvgTextGlyphPositions(mVg, 0, 0, start, start + length, glyphs.data(), (int) length); + + size_t byte = 0; + for (int index = 0; index < count; index++) { + const size_t glyphStart = (size_t) (glyphs[(size_t) index].str - start); + const halnf pen = glyphs[(size_t) index].x; + for (; byte <= glyphStart && byte < length; byte++) { + widths[byte] = pen; + } + } + const halnf total = nvgTextBounds(mVg, 0, 0, start, start + length, nullptr); + for (; byte < length; byte++) { + widths[byte] = total; + } + widths[length] = total; + return widths; +} + +void NanoVgCanvasBackend::drawImage(const RectF& rect, ImageHandle image, halnf angle, halnf alpha, halnf rounding) { + const NVGpaint paint = nvgImagePattern(mVg, rect.x, rect.y, rect.z, rect.w, angle, image.id(), alpha); + nvgBeginPath(mVg); + nvgRoundedRect(mVg, rect.x, rect.y, rect.z, rect.w, rounding); + nvgFillPaint(mVg, paint); + nvgFill(mVg); +} +ImageHandle NanoVgCanvasBackend::createImageFromRenderTarget(uint32_t renderTargetId) { + RenderTargetEntry entry; + if (!mSurface.gpu().renderTargets().resolve(renderTargetId, entry)) { + return {}; + } + const int image = nvgCreateImageFromHandleVk( + mVg, entry.image, entry.view, entry.sampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, entry.width, entry.height, 0 + ); + return makeImageHandle(image); +} + +void NanoVgCanvasBackend::updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId) { + RenderTargetEntry entry; + if (!mSurface.gpu().renderTargets().resolve(renderTargetId, entry)) { + return; + } + nvgUpdateImageFromHandleVk( + mVg, + image.id(), + entry.image, + entry.view, + entry.sampler, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + entry.width, + entry.height + ); +} + +ImageHandle NanoVgCanvasBackend::createImageFromFile(const char* path) { + return makeImageHandle(nvgCreateImage(mVg, path, 0)); +} + +ImageHandle NanoVgCanvasBackend::createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba) { + return makeImageHandle(nvgCreateImageRGBA(mVg, width, height, 0, rgba)); +} + +void NanoVgCanvasBackend::updateImageRGBA(ImageHandle image, const unsigned char* rgba) { + nvgUpdateImage(mVg, image.id(), rgba); +} + +Vec2F NanoVgCanvasBackend::imageSize(ImageHandle image) { + int width = 0; + int height = 0; + nvgImageSize(mVg, image.id(), &width, &height); + return { (halnf) width, (halnf) height }; +} + +void NanoVgCanvasBackend::deleteImage(ImageHandle image) { nvgDeleteImage(mVg, image.id()); } diff --git a/GraphicApplication/src/canvas/backends/nanovg/private/NanoVgCanvasBackend.hpp b/GraphicApplication/src/canvas/backends/nanovg/private/NanoVgCanvasBackend.hpp new file mode 100644 index 00000000..9cef5ff0 --- /dev/null +++ b/GraphicApplication/src/canvas/backends/nanovg/private/NanoVgCanvasBackend.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "CanvasBackend.hpp" +#include "CanvasFontLoader.hpp" +#include "WindowFrame.hpp" +#include "WindowSurface.hpp" + +#include + +#include + +namespace tp { + + class Profiler; + + // Draws a canvas onto a window surface with NanoVG on Vulkan. + class NanoVgCanvasBackend final : public CanvasBackend { + public: + NanoVgCanvasBackend(WindowSurface& surface, Profiler& profiler, const std::filesystem::path& resourceDirectory); + ~NanoVgCanvasBackend() override; + + NanoVgCanvasBackend(const NanoVgCanvasBackend&) = delete; + NanoVgCanvasBackend& operator=(const NanoVgCanvasBackend&) = delete; + + void beginFrame(const GpuFrame& frame, const WindowFrame& windowFrame); + void endFrame(); + + void setClip(std::optional clip) override; + void setCompositeOperation(CompositeOperation operation) override; + + void beginPath() override; + void addPathRect(const RectF& rect, halnf round) override; + void setPathWinding(PathWinding winding) override; + void fillPath(const RGBA& color) override; + void fillPath(const BoxGradient& gradient) override; + + void rect(const RectF& rect, const RGBA& color, halnf round) override; + void gradientRect(const RectF& rect, const RGBA& from, const RGBA& to, Axis axis, halnf round) override; + void outline(const RectF& rect, const RGBA& color, halnf round, halnf width) override; + void circle(Vec2F center, halnf radius, const RGBA& color) override; + void circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width) override; + void line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness) override; + void polyline(const Vec2F* points, int count, const RGBA& color, halnf width) override; + void polygon(const Vec2F* points, int count, const RGBA& color) override; + void polygon(const Vec2F* points, int count, const LinearGradient& gradient) override; + void ringSegment( + Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient + ) override; + void radialGlow( + Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor + ) override; + + void text(const std::string& text, const RectF& area, const TextStyle& style) override; + void rotatedText(const std::string& text, Vec2F center, halnf size, halnf angle, const RGBA& color) override; + + [[nodiscard]] halnf textWidth(const std::string& text, halnf size, FontFace face) const override; + [[nodiscard]] std::vector prefixWidths(const std::string& text, halnf size, FontFace face) const override; + + [[nodiscard]] ImageHandle createImageFromRenderTarget(uint32_t renderTargetId) override; + void updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId) override; + [[nodiscard]] ImageHandle createImageFromFile(const char* path) override; + [[nodiscard]] ImageHandle createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba) override; + void updateImageRGBA(ImageHandle image, const unsigned char* rgba) override; + [[nodiscard]] Vec2F imageSize(ImageHandle image) override; + void deleteImage(ImageHandle image) override; + void drawImage(const RectF& rect, ImageHandle image, halnf angle, halnf alpha, halnf rounding) override; + + private: + NVGcontext* mVg = nullptr; + WindowSurface& mSurface; + Profiler& mProfiler; + uint32_t mFlushSection = 0; + CanvasFontIds mFonts; + + // NanoVG reads the recording command buffer and frame slot through these pointers on every flush. + VkCommandBuffer mFrameCommandBuffers[WindowSurface::kFramesInFlight] = {}; + uint32_t mFrameSlot = 0; + }; + + inline NVGcolor toNVGColor(const RGBA& color) { return { color.r, color.g, color.b, color.a }; } +} diff --git a/GraphicApplication/src/canvas/backends/recording/private/RecordingCanvasBackend.cpp b/GraphicApplication/src/canvas/backends/recording/private/RecordingCanvasBackend.cpp new file mode 100644 index 00000000..61ef7f2d --- /dev/null +++ b/GraphicApplication/src/canvas/backends/recording/private/RecordingCanvasBackend.cpp @@ -0,0 +1,211 @@ +#include "RecordingCanvasBackend.hpp" + +#include + +using namespace tp; + +namespace { + RectF boundsOf(const Vec2F* points, int count) { + if (count <= 0) { + return { 0, 0, 0, 0 }; + } + Vec2F low = points[0]; + Vec2F high = points[0]; + for (int index = 1; index < count; index++) { + low = { std::min(low.x, points[index].x), std::min(low.y, points[index].y) }; + high = { std::max(high.x, points[index].x), std::max(high.y, points[index].y) }; + } + return { low, high - low }; + } + + RectF squareAround(Vec2F center, halnf radius) { + return { center - Vec2F(radius, radius), Vec2F(radius, radius) * 2 }; + } +} + +void RecordingCanvasBackend::setClip(std::optional clip) { mClip = clip; } + +CanvasCommand& RecordingCanvasBackend::record(CanvasCommand::Kind kind, const RectF& rect, const RGBA& color) { + CanvasCommand command; + command.kind = kind; + command.rect = rect; + command.color = color; + command.clipped = mClip.has_value(); + command.clip = mClip.value_or(RectF(-1.0e9f, -1.0e9f, 2.0e9f, 2.0e9f)); + command.composite = mComposite; + mCommands.push_back(std::move(command)); + return mCommands.back(); +} + +void RecordingCanvasBackend::recordPoints( + CanvasCommand::Kind kind, const Vec2F* points, int count, const RGBA& color, halnf width +) { + CanvasCommand& command = record(kind, boundsOf(points, count), color); + command.width = width; + command.points.assign(points, points + std::max(count, 0)); +} + +void RecordingCanvasBackend::beginPath() { mPathRects.clear(); } + +void RecordingCanvasBackend::addPathRect(const RectF& rect, halnf) { mPathRects.push_back(rect); } + +void RecordingCanvasBackend::fillPath(const RGBA& color) { + for (const RectF& rect : mPathRects) { + record(CanvasCommand::Kind::Path, rect, color); + } +} + +void RecordingCanvasBackend::fillPath(const BoxGradient& gradient) { + CanvasCommand& command = record(CanvasCommand::Kind::Path, gradient.box, gradient.innerColor); + command.endColor = gradient.outerColor; + command.round = gradient.radius; +} + +void RecordingCanvasBackend::rect(const RectF& rect, const RGBA& color, halnf round) { + record(CanvasCommand::Kind::Rect, rect, color).round = round; +} + +void RecordingCanvasBackend::gradientRect(const RectF& rect, const RGBA& from, const RGBA& to, Axis, halnf round) { + CanvasCommand& command = record(CanvasCommand::Kind::Gradient, rect, from); + command.endColor = to; + command.round = round; +} + +void RecordingCanvasBackend::outline(const RectF& rect, const RGBA& color, halnf round, halnf width) { + CanvasCommand& command = record(CanvasCommand::Kind::Outline, rect, color); + command.round = round; + command.width = width; +} + +void RecordingCanvasBackend::circle(Vec2F center, halnf radius, const RGBA& color) { + record(CanvasCommand::Kind::Circle, squareAround(center, radius), color); +} + +void RecordingCanvasBackend::circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width) { + record(CanvasCommand::Kind::CircleOutline, squareAround(center, radius), color).width = width; +} + +void RecordingCanvasBackend::line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness) { + const Vec2F points[] = { start, end }; + recordPoints(CanvasCommand::Kind::Line, points, 2, color, thickness); +} + +void RecordingCanvasBackend::polyline(const Vec2F* points, int count, const RGBA& color, halnf width) { + recordPoints(CanvasCommand::Kind::Polyline, points, count, color, width); +} + +void RecordingCanvasBackend::polygon(const Vec2F* points, int count, const RGBA& color) { + recordPoints(CanvasCommand::Kind::Polygon, points, count, color, 0); +} + +void RecordingCanvasBackend::polygon(const Vec2F* points, int count, const LinearGradient& gradient) { + recordPoints(CanvasCommand::Kind::Polygon, points, count, gradient.fromColor, 0); + mCommands.back().endColor = gradient.toColor; +} + +void RecordingCanvasBackend::ringSegment( + Vec2F center, halnf, halnf outerRadius, halnf, halnf, const LinearGradient& gradient +) { + CanvasCommand& command = + record(CanvasCommand::Kind::RingSegment, squareAround(center, outerRadius), gradient.fromColor); + command.endColor = gradient.toColor; +} + +void RecordingCanvasBackend::radialGlow( + Vec2F center, halnf, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor +) { + record(CanvasCommand::Kind::RadialGlow, squareAround(center, outerRadius), innerColor).endColor = outerColor; +} + +void RecordingCanvasBackend::text(const std::string& text, const RectF& area, const TextStyle& style) { + CanvasCommand& command = record(CanvasCommand::Kind::Text, area, style.color); + command.text = text; + command.face = style.face; + command.fontSize = style.size; + command.align = style.align; +} + +void RecordingCanvasBackend::rotatedText(const std::string& text, Vec2F center, halnf size, halnf, const RGBA& color) { + const halnf width = textWidth(text, size, FontFace::Regular); + CanvasCommand& command = + record(CanvasCommand::Kind::RotatedText, { center - Vec2F(width, size) / 2, Vec2F(width, size) }, color); + command.text = text; + command.fontSize = size; +} + +halnf RecordingCanvasBackend::textWidth(const std::string& text, halnf size, FontFace) const { + mTextMeasurements++; + return (halnf) text.size() * size * kCharacterWidthFactor; +} + +std::vector RecordingCanvasBackend::prefixWidths(const std::string& text, halnf size, FontFace) const { + mTextMeasurements++; + std::vector widths(text.size() + 1); + for (size_t index = 0; index <= text.size(); index++) { + widths[index] = (halnf) index * size * kCharacterWidthFactor; + } + return widths; +} + +ImageHandle RecordingCanvasBackend::addImage(Vec2F size) { + const int id = (int) mImages.size() + 1; + mImages.push_back({ id, size, true }); + return makeImageHandle(id); +} + +ImageHandle RecordingCanvasBackend::createImageFromRenderTarget(uint32_t) { return addImage({ 1, 1 }); } + +ImageHandle RecordingCanvasBackend::createImageFromFile(const char* path) { + for (const ImageFile& file : mImageFiles) { + if (file.path == path) { + return addImage(file.size); + } + } + return {}; +} + +ImageHandle RecordingCanvasBackend::createImageFromMemoryRGBA(int width, int height, const unsigned char*) { + if (width <= 0 || height <= 0) { + return {}; + } + return addImage({ (halnf) width, (halnf) height }); +} + +Vec2F RecordingCanvasBackend::imageSize(ImageHandle image) { + const size_t index = (size_t) image.id() - 1; + if (index >= mImages.size() || !mImages[index].live) { + return { 0, 0 }; + } + return mImages[index].size; +} + +void RecordingCanvasBackend::deleteImage(ImageHandle image) { + const size_t index = (size_t) image.id() - 1; + if (index < mImages.size()) { + mImages[index].live = false; + } +} + +void RecordingCanvasBackend::drawImage(const RectF& rect, ImageHandle image, halnf, halnf, halnf rounding) { + CanvasCommand& command = record(CanvasCommand::Kind::Image, rect, RGBA(1, 1, 1, 1)); + command.image = image; + command.round = rounding; +} + +void RecordingCanvasBackend::addImageFile(const std::string& path, Vec2F size) { + mImageFiles.push_back({ path, size }); +} + +const CanvasCommand* RecordingCanvasBackend::findText(const std::string& text) const { + for (const CanvasCommand& command : mCommands) { + const bool isText = command.kind == CanvasCommand::Kind::Text || command.kind == CanvasCommand::Kind::RotatedText; + if (isText && command.text == text) { + return &command; + } + } + return nullptr; +} + +size_t RecordingCanvasBackend::liveImageCount() const { + return (size_t) std::count_if(mImages.begin(), mImages.end(), [](const Image& image) { return image.live; }); +} diff --git a/GraphicApplication/src/canvas/backends/recording/public/RecordingCanvasBackend.hpp b/GraphicApplication/src/canvas/backends/recording/public/RecordingCanvasBackend.hpp new file mode 100644 index 00000000..5096e027 --- /dev/null +++ b/GraphicApplication/src/canvas/backends/recording/public/RecordingCanvasBackend.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include "CanvasBackend.hpp" + +#include +#include +#include + +namespace tp { + + // One drawing command as a recording canvas saw it, in surface coordinates. + struct CanvasCommand { + enum class Kind { + Rect, + Gradient, + Outline, + Circle, + CircleOutline, + Line, + Polyline, + Polygon, + RingSegment, + RadialGlow, + Text, + RotatedText, + Image, + Path + }; + + Kind kind = Kind::Rect; + RectF rect; + RGBA color; + RGBA endColor; + halnf round = 0; + halnf width = 0; + std::string text; + FontFace face = FontFace::Regular; + halnf fontSize = 0; + TextAlign align; + std::vector points; + ImageHandle image; + RectF clip; + bool clipped = false; + CanvasBackend::CompositeOperation composite = CanvasBackend::CompositeOperation::SourceOver; + }; + + // Records drawing instead of rendering it, and measures text with a fixed advance per character, so layout and + // painting can be tested without a GPU. + class RecordingCanvasBackend final : public CanvasBackend { + public: + static constexpr halnf kCharacterWidthFactor = 0.5f; + + void setClip(std::optional clip) override; + void setCompositeOperation(CompositeOperation operation) override { mComposite = operation; } + + void beginPath() override; + void addPathRect(const RectF& rect, halnf round) override; + void setPathWinding(PathWinding) override {} + void fillPath(const RGBA& color) override; + void fillPath(const BoxGradient& gradient) override; + + void rect(const RectF& rect, const RGBA& color, halnf round) override; + void gradientRect(const RectF& rect, const RGBA& from, const RGBA& to, Axis axis, halnf round) override; + void outline(const RectF& rect, const RGBA& color, halnf round, halnf width) override; + void circle(Vec2F center, halnf radius, const RGBA& color) override; + void circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width) override; + void line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness) override; + void polyline(const Vec2F* points, int count, const RGBA& color, halnf width) override; + void polygon(const Vec2F* points, int count, const RGBA& color) override; + void polygon(const Vec2F* points, int count, const LinearGradient& gradient) override; + void ringSegment( + Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient + ) override; + void radialGlow( + Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor + ) override; + + void text(const std::string& text, const RectF& area, const TextStyle& style) override; + void rotatedText(const std::string& text, Vec2F center, halnf size, halnf angle, const RGBA& color) override; + + [[nodiscard]] halnf textWidth(const std::string& text, halnf size, FontFace face) const override; + [[nodiscard]] std::vector prefixWidths(const std::string& text, halnf size, FontFace face) const override; + + [[nodiscard]] ImageHandle createImageFromRenderTarget(uint32_t renderTargetId) override; + void updateImageFromRenderTarget(ImageHandle, uint32_t) override {} + [[nodiscard]] ImageHandle createImageFromFile(const char* path) override; + [[nodiscard]] ImageHandle createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba) override; + void updateImageRGBA(ImageHandle, const unsigned char*) override {} + [[nodiscard]] Vec2F imageSize(ImageHandle image) override; + void deleteImage(ImageHandle image) override; + void drawImage(const RectF& rect, ImageHandle image, halnf angle, halnf alpha, halnf rounding) override; + + // Gives files at this path a size so image loading can be tested; other paths fail to load. + void addImageFile(const std::string& path, Vec2F size); + + [[nodiscard]] const std::vector& commands() const { return mCommands; } + [[nodiscard]] const CanvasCommand* findText(const std::string& text) const; + [[nodiscard]] bool hasText(const std::string& text) const { return findText(text) != nullptr; } + [[nodiscard]] size_t textMeasurements() const { return mTextMeasurements; } + [[nodiscard]] size_t liveImageCount() const; + void clear() { mCommands.clear(); } + + private: + struct Image { + int id = 0; + Vec2F size = { 0, 0 }; + bool live = true; + }; + + struct ImageFile { + std::string path; + Vec2F size; + }; + + CanvasCommand& record(CanvasCommand::Kind kind, const RectF& rect, const RGBA& color); + void recordPoints(CanvasCommand::Kind kind, const Vec2F* points, int count, const RGBA& color, halnf width); + ImageHandle addImage(Vec2F size); + + std::vector mCommands; + std::vector mImages; + std::vector mImageFiles; + std::vector mPathRects; + std::optional mClip; + CompositeOperation mComposite = CompositeOperation::SourceOver; + mutable size_t mTextMeasurements = 0; + }; +} diff --git a/GraphicApplication/src/canvas/private/Canvas.cpp b/GraphicApplication/src/canvas/private/Canvas.cpp index 88008b93..71226e9a 100644 --- a/GraphicApplication/src/canvas/private/Canvas.cpp +++ b/GraphicApplication/src/canvas/private/Canvas.cpp @@ -1,110 +1,22 @@ -#include "CanvasContext.hpp" - -#include "GpuError.hpp" -#include "Profiler.hpp" - -#include - -#include +#include "Canvas.hpp" using namespace tp; -namespace { - constexpr halnf kItalicSkew = -0.21f; +Canvas::Canvas(std::unique_ptr backend) : + mBackend(std::move(backend)) {} - bool isItalic(FontFace face) { return face == FontFace::Italic || face == FontFace::BoldItalic; } - - void addRect(NVGcontext* vg, const RectF& rect, halnf round) { - if (round == 0) { - nvgRect(vg, rect.x, rect.y, rect.z, rect.w); - } else { - nvgRoundedRect(vg, rect.x, rect.y, rect.z, rect.w, round); - } - } - - void stroke(NVGcontext* vg, const RGBA& color, halnf width) { - nvgStrokeWidth(vg, width); - nvgStrokeColor(vg, toNVGColor(color)); - nvgStroke(vg); - } - - void fill(NVGcontext* vg, const RGBA& color) { - nvgFillColor(vg, toNVGColor(color)); - nvgFill(vg); - } - - NVGpaint linearPaint(NVGcontext* vg, const Canvas::LinearGradient& gradient, Vec2F origin) { - const Vec2F from = gradient.from + origin; - const Vec2F to = gradient.to + origin; - return nvgLinearGradient( - vg, from.x, from.y, to.x, to.y, toNVGColor(gradient.fromColor), toNVGColor(gradient.toColor) - ); - } - - int nvgCompositeOperationOf(Canvas::CompositeOperation operation) { - switch (operation) { - case Canvas::CompositeOperation::SourceOver: return NVG_SOURCE_OVER; - case Canvas::CompositeOperation::DestinationOut: return NVG_DESTINATION_OUT; - case Canvas::CompositeOperation::DestinationOver: return NVG_DESTINATION_OVER; - } - return NVG_SOURCE_OVER; - } - - int nvgWindingOf(Canvas::PathWinding winding) { return winding == Canvas::PathWinding::Hole ? NVG_HOLE : NVG_SOLID; } -} - -Canvas::Canvas(WindowSurface& surface, Profiler& profiler, const std::filesystem::path& resourceDirectory) : - mContext(std::make_unique()) { - mContext->surface = &surface; - mContext->profiler = &profiler; - mContext->flushSection = profiler.section("frame.ui_flush"); - - const GpuDevice& gpu = surface.gpu(); - VKNVGCreateInfo createInfo{}; - createInfo.gpu = gpu.physicalDevice(); - createInfo.device = gpu.device(); - createInfo.renderpass = surface.renderPass(); - createInfo.cmdBuffer = mContext->frameCommandBuffers; - createInfo.frameCount = WindowSurface::kFramesInFlight; - createInfo.currentFrame = &mContext->frameSlot; - createInfo.commandPool = gpu.commandPool(); - createInfo.graphicsQueueFamilyIndex = gpu.graphicsQueueFamily(); - - mContext->vg = nvgCreateVk(&createInfo, NVG_ANTIALIAS | NVG_STENCIL_STROKES, gpu.graphicsQueue()); - if (!mContext->vg) { - throw GpuError("creating the NanoVG context", VK_ERROR_INITIALIZATION_FAILED); - } - mContext->fonts = loadCanvasFonts(mContext->vg, resourceDirectory); -} - -Canvas::~Canvas() { - mContext->surface->gpu().waitIdle(); - nvgDeleteVk(mContext->vg); -} - -void Canvas::drawBegin(const GpuFrame& frame, const WindowFrame& windowFrame) { - mContext->frameCommandBuffers[frame.slot] = frame.commandBuffer; - mContext->frameSlot = frame.slot; - - const Vec2F surfaceSize = windowFrame.surfaceSize(); - const halnf devicePixelRatio = surfaceSize.x > 0 ? (halnf) frame.extent.width / surfaceSize.x : 1.0f; - nvgBeginFrame(mContext->vg, surfaceSize.x, surfaceSize.y, devicePixelRatio); - nvgTranslate(mContext->vg, windowFrame.margin(), windowFrame.margin()); +Canvas::~Canvas() = default; +void Canvas::beginFrame() { mOrigin = { 0, 0 }; mClips.clear(); mSavedFrames.clear(); + mComposite = CompositeOperation::SourceOver; + mBackend->setClip(std::nullopt); + mBackend->setCompositeOperation(mComposite); } -void Canvas::drawEnd() { - const ProfileScope scope(*mContext->profiler, mContext->flushSection); - nvgEndFrame(mContext->vg); -} - -void Canvas::save() { - nvgSave(mContext->vg); - mSavedFrames.append({ mOrigin, mClips.size() }); -} +void Canvas::save() { mSavedFrames.append({ mOrigin, mClips.size(), mComposite }); } void Canvas::restore() { DEBUG_ASSERT(mSavedFrames.size()); @@ -115,203 +27,23 @@ void Canvas::restore() { mClips.pop(); } mOrigin = saved.origin; - nvgRestore(mContext->vg); applyClip(); -} - -void Canvas::setCompositeOperation(CompositeOperation operation) { - nvgGlobalCompositeOperation(mContext->vg, nvgCompositeOperationOf(operation)); -} - -void Canvas::beginPath() { nvgBeginPath(mContext->vg); } - -void Canvas::addPathRect(RectF rect, halnf round) { - rect.pos += mOrigin; - addRect(mContext->vg, rect, round); -} - -void Canvas::setPathWinding(PathWinding winding) { nvgPathWinding(mContext->vg, nvgWindingOf(winding)); } - -void Canvas::fillPath(const RGBA& color) { fill(mContext->vg, color); } - -void Canvas::fillPath(const BoxGradient& gradient) { - const RectF box = { gradient.box.pos + mOrigin, gradient.box.size }; - const NVGpaint paint = nvgBoxGradient( - mContext->vg, - box.x, - box.y, - box.z, - box.w, - gradient.radius, - gradient.feather, - toNVGColor(gradient.innerColor), - toNVGColor(gradient.outerColor) - ); - nvgFillPaint(mContext->vg, paint); - nvgFill(mContext->vg); -} - -void Canvas::rect(RectF rect, const RGBA& color, halnf round) { - rect.pos += mOrigin; - nvgBeginPath(mContext->vg); - addRect(mContext->vg, rect, round); - fill(mContext->vg, color); -} - -void Canvas::gradientRect(RectF rect, const RGBA& from, const RGBA& to, bool horizontal, halnf round) { - rect.pos += mOrigin; - NVGcontext* vg = mContext->vg; - const halnf endX = horizontal ? rect.x + rect.z : rect.x; - const halnf endY = horizontal ? rect.y : rect.y + rect.w; - const NVGpaint paint = nvgLinearGradient(vg, rect.x, rect.y, endX, endY, toNVGColor(from), toNVGColor(to)); - nvgBeginPath(vg); - addRect(vg, rect, round); - nvgFillPaint(vg, paint); - nvgFill(vg); -} - -void Canvas::debugCross(RectF rect, const RGBA& color) { - rect.pos += mOrigin; - NVGcontext* vg = mContext->vg; - nvgBeginPath(vg); - nvgMoveTo(vg, rect.p1().x, rect.p1().y); - nvgLineTo(vg, rect.p3().x, rect.p3().y); - nvgMoveTo(vg, rect.p2().x, rect.p2().y); - nvgLineTo(vg, rect.p4().x, rect.p4().y); - stroke(vg, color, 2); -} - -void Canvas::frame(RectF rect, const RGBA& color) { - rect.pos += mOrigin; - NVGcontext* vg = mContext->vg; - nvgBeginPath(vg); - nvgRect(vg, rect.x, rect.y, rect.z, rect.w); - stroke(vg, color, 2); -} - -void Canvas::outline(RectF rect, const RGBA& color, halnf round, halnf width) { - rect.pos += mOrigin; - const halnf inset = width / 2; - nvgBeginPath(mContext->vg); - nvgRoundedRect( - mContext->vg, rect.x + inset, rect.y + inset, rect.z - width, rect.w - width, round > inset ? round - inset : 0 - ); - stroke(mContext->vg, color, width); -} - -void Canvas::circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width) { - center += mOrigin; - nvgBeginPath(mContext->vg); - nvgCircle(mContext->vg, center.x, center.y, radius - width / 2); - stroke(mContext->vg, color, width); -} - -void Canvas::polyline(const Vec2F* points, int count, const RGBA& color, halnf width) { - if (count < 2) { - return; + if (mComposite != saved.composite) { + mComposite = saved.composite; + mBackend->setCompositeOperation(mComposite); } - - nvgBeginPath(mContext->vg); - nvgMoveTo(mContext->vg, points[0].x + mOrigin.x, points[0].y + mOrigin.y); - for (int index = 1; index < count; index++) { - nvgLineTo(mContext->vg, points[index].x + mOrigin.x, points[index].y + mOrigin.y); - } - nvgLineCap(mContext->vg, NVG_ROUND); - nvgLineJoin(mContext->vg, NVG_ROUND); - stroke(mContext->vg, color, width); -} - -void Canvas::polygon(const Vec2F* points, int count, const RGBA& color) { - if (count < 3) { - return; - } - - nvgBeginPath(mContext->vg); - nvgMoveTo(mContext->vg, points[0].x + mOrigin.x, points[0].y + mOrigin.y); - for (int index = 1; index < count; index++) { - nvgLineTo(mContext->vg, points[index].x + mOrigin.x, points[index].y + mOrigin.y); - } - nvgClosePath(mContext->vg); - fill(mContext->vg, color); -} - -void Canvas::polygon(const Vec2F* points, int count, const LinearGradient& gradient) { - if (count < 3) { - return; - } - - nvgBeginPath(mContext->vg); - nvgMoveTo(mContext->vg, points[0].x + mOrigin.x, points[0].y + mOrigin.y); - for (int index = 1; index < count; index++) { - nvgLineTo(mContext->vg, points[index].x + mOrigin.x, points[index].y + mOrigin.y); - } - nvgClosePath(mContext->vg); - nvgFillPaint(mContext->vg, linearPaint(mContext->vg, gradient, mOrigin)); - nvgFill(mContext->vg); -} - -void Canvas::ringSegment( - Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient -) { - center += mOrigin; - - nvgBeginPath(mContext->vg); - nvgArc(mContext->vg, center.x, center.y, innerRadius, fromAngle, toAngle, NVG_CW); - nvgArc(mContext->vg, center.x, center.y, outerRadius, toAngle, fromAngle, NVG_CCW); - nvgClosePath(mContext->vg); - nvgFillPaint(mContext->vg, linearPaint(mContext->vg, gradient, mOrigin)); - nvgFill(mContext->vg); -} - -void Canvas::radialGlow( - Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor -) { - center += mOrigin; - - const NVGpaint paint = nvgRadialGradient( - mContext->vg, center.x, center.y, innerRadius, outerRadius, toNVGColor(innerColor), toNVGColor(outerColor) - ); - - nvgBeginPath(mContext->vg); - nvgCircle(mContext->vg, center.x, center.y, outerRadius); - nvgFillPaint(mContext->vg, paint); - nvgFill(mContext->vg); -} - -void Canvas::line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness) { - start += mOrigin; - end += mOrigin; - nvgBeginPath(mContext->vg); - nvgMoveTo(mContext->vg, start.x, start.y); - nvgLineTo(mContext->vg, end.x, end.y); - stroke(mContext->vg, color, thickness); -} - -void Canvas::circle(Vec2F center, halnf radius, const RGBA& color) { - center += mOrigin; - nvgBeginPath(mContext->vg); - nvgCircle(mContext->vg, center.x, center.y, radius); - fill(mContext->vg, color); } void Canvas::clipTo(const RectF& rect) { - const RectF placed = { rect.pos + mOrigin, rect.size }; - RectF clip = placed; + RectF clip = placed(rect); if (mClips.size()) { - mClips.last().calcIntersection(placed, clip); + mClips.last().calcIntersection(placed(rect), clip); } mClips.append(clip); applyClip(); } -void Canvas::applyClip() { - if (!mClips.size()) { - nvgResetScissor(mContext->vg); - return; - } - const RectF& clip = mClips.last(); - nvgScissor(mContext->vg, clip.x, clip.y, clip.z, clip.w); -} +void Canvas::applyClip() { mBackend->setClip(mClips.size() ? std::optional(mClips.last()) : std::nullopt); } bool Canvas::isClippedAway() const { return mClips.size() && mClips.last().size.length2() <= EPSILON; } @@ -319,115 +51,148 @@ bool Canvas::isOutsideClip(const RectF& rect) const { if (!mClips.size()) { return false; } - const RectF placed = { rect.pos + mOrigin, rect.size }; + const RectF area = placed(rect); const RectF& clip = mClips.last(); - return placed.x >= clip.x + clip.z || clip.x >= placed.x + placed.z || placed.y >= clip.y + clip.w || - clip.y >= placed.y + placed.w; + return area.x >= clip.x + clip.z || clip.x >= area.x + area.z || area.y >= clip.y + clip.w || + clip.y >= area.y + area.w; } -void Canvas::text(const char* string, const RectF& area, const TextStyle& style) { +std::vector Canvas::placed(const std::vector& points) const { + std::vector result; + result.reserve(points.size()); + for (const Vec2F& point : points) { + result.push_back(point + mOrigin); + } + return result; +} + +Canvas::LinearGradient Canvas::placed(const LinearGradient& gradient) const { + return { gradient.from + mOrigin, gradient.to + mOrigin, gradient.fromColor, gradient.toColor }; +} + +void Canvas::setCompositeOperation(CompositeOperation operation) { + mComposite = operation; + mBackend->setCompositeOperation(operation); +} + +void Canvas::beginPath() { mBackend->beginPath(); } + +void Canvas::addPathRect(RectF rect, halnf round) { mBackend->addPathRect(placed(rect), round); } + +void Canvas::setPathWinding(PathWinding winding) { mBackend->setPathWinding(winding); } + +void Canvas::fillPath(const RGBA& color) { mBackend->fillPath(color); } + +void Canvas::fillPath(const BoxGradient& gradient) { + BoxGradient placedGradient = gradient; + placedGradient.box = placed(gradient.box); + mBackend->fillPath(placedGradient); +} + +void Canvas::rect(RectF rect, const RGBA& color, halnf round) { mBackend->rect(placed(rect), color, round); } + +void Canvas::gradientRect(RectF rect, const RGBA& from, const RGBA& to, Axis axis, halnf round) { + mBackend->gradientRect(placed(rect), from, to, axis, round); +} + +void Canvas::outline(RectF rect, const RGBA& color, halnf round, halnf width) { + mBackend->outline(placed(rect), color, round, width); +} + +void Canvas::circle(Vec2F center, halnf radius, const RGBA& color) { + mBackend->circle(center + mOrigin, radius, color); +} + +void Canvas::circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width) { + mBackend->circleOutline(center + mOrigin, radius, color, width); +} + +void Canvas::line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness) { + mBackend->line(start + mOrigin, end + mOrigin, color, thickness); +} + +void Canvas::polyline(const std::vector& points, const RGBA& color, halnf width) { + const std::vector placedPoints = placed(points); + mBackend->polyline(placedPoints.data(), (int) placedPoints.size(), color, width); +} + +void Canvas::polygon(const std::vector& points, const RGBA& color) { + const std::vector placedPoints = placed(points); + mBackend->polygon(placedPoints.data(), (int) placedPoints.size(), color); +} + +void Canvas::polygon(const std::vector& points, const LinearGradient& gradient) { + const std::vector placedPoints = placed(points); + mBackend->polygon(placedPoints.data(), (int) placedPoints.size(), placed(gradient)); +} + +void Canvas::ringSegment( + Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient +) { + mBackend->ringSegment(center + mOrigin, innerRadius, outerRadius, fromAngle, toAngle, placed(gradient)); +} + +void Canvas::radialGlow( + Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor +) { + mBackend->radialGlow(center + mOrigin, innerRadius, outerRadius, innerColor, outerColor); +} + +void Canvas::text(const std::string& string, const RectF& area, const TextStyle& style) { const halnf padding = style.padding; const RectF padded = { area.x + padding, area.y + padding, area.z - padding * 2, area.w - padding * 2 }; if (isOutsideClip(padded)) { return; } - const SavedState saved(*this); - clipTo(padded); - const RectF rect = { padded.pos + mOrigin, padded.size }; - - NVGcontext* vg = mContext->vg; - nvgFontSize(vg, style.size); - nvgFontFaceId(vg, mContext->fonts.idFor(style.face)); - nvgFillColor(vg, toNVGColor(style.color)); - - halnf anchorX = rect.x; - halnf anchorY = rect.y; - int alignment = 0; - switch (style.align.horizontal) { - case HorizontalAlign::Left: alignment |= NVG_ALIGN_LEFT; break; - case HorizontalAlign::Center: - alignment |= NVG_ALIGN_CENTER; - anchorX += rect.z * 0.5f; - break; - case HorizontalAlign::Right: - alignment |= NVG_ALIGN_RIGHT; - anchorX += rect.z; - break; - } - switch (style.align.vertical) { - case VerticalAlign::Top: alignment |= NVG_ALIGN_TOP; break; - case VerticalAlign::Center: - alignment |= NVG_ALIGN_MIDDLE; - anchorY += rect.w * 0.5f; - break; - case VerticalAlign::Bottom: - alignment |= NVG_ALIGN_BOTTOM; - anchorY += rect.w; - break; - } - nvgTextAlign(vg, alignment); - - if (!isItalic(style.face)) { - nvgText(vg, anchorX, anchorY, string, nullptr); - return; - } - nvgTranslate(vg, anchorX, anchorY); - nvgSkewX(vg, kItalicSkew); - nvgText(vg, 0, 0, string, nullptr); + const Clip clip(*this, padded); + mBackend->text(string, placed(padded), style); } -void Canvas::rotatedText(const char* string, Vec2F center, halnf size, halnf angle, const RGBA& color) { - center += mOrigin; - - nvgSave(mContext->vg); - nvgTranslate(mContext->vg, center.x, center.y); - nvgRotate(mContext->vg, angle); - nvgFontSize(mContext->vg, size); - nvgFontFaceId(mContext->vg, mContext->fonts.regular); - nvgTextAlign(mContext->vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgFillColor(mContext->vg, toNVGColor(color)); - nvgText(mContext->vg, 0, 0, string, nullptr); - nvgRestore(mContext->vg); +void Canvas::rotatedText(const std::string& string, Vec2F center, halnf size, halnf angle, const RGBA& color) { + mBackend->rotatedText(string, center + mOrigin, size, angle, color); } -halnf Canvas::textWidth(const char* string, halnf size, FontFace face) { - nvgFontSize(mContext->vg, size); - nvgFontFaceId(mContext->vg, mContext->fonts.idFor(face)); - return nvgTextBounds(mContext->vg, 0, 0, string, nullptr, nullptr); +halnf Canvas::textWidth(const std::string& string, halnf size, FontFace face) const { + return mBackend->textWidth(string, size, face); } -void Canvas::prefixWidths(const char* string, size_t length, halnf size, FontFace face, halnf* widths) { - nvgFontSize(mContext->vg, size); - nvgFontFaceId(mContext->vg, mContext->fonts.idFor(face)); - nvgTextAlign(mContext->vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); +std::vector Canvas::prefixWidths(const std::string& string, halnf size, FontFace face) const { + return mBackend->prefixWidths(string, size, face); +} - std::vector glyphs(length); - const int count = nvgTextGlyphPositions(mContext->vg, 0, 0, string, string + length, glyphs.data(), (int) length); +ImageHandle Canvas::createImageFromRenderTarget(uint32_t renderTargetId) { + return mBackend->createImageFromRenderTarget(renderTargetId); +} - size_t byte = 0; - for (int index = 0; index < count; index++) { - const size_t glyphStart = (size_t) (glyphs[(size_t) index].str - string); - const halnf pen = glyphs[(size_t) index].x; - for (; byte <= glyphStart && byte < length; byte++) { - widths[byte] = pen; - } +void Canvas::updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId) { + if (image.isValid()) { + mBackend->updateImageFromRenderTarget(image, renderTargetId); } - const halnf total = nvgTextBounds(mContext->vg, 0, 0, string, string + length, nullptr); - for (; byte < length; byte++) { - widths[byte] = total; +} + +ImageHandle Canvas::createImageFromFile(const char* path) { return mBackend->createImageFromFile(path); } + +ImageHandle Canvas::createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba) { + return mBackend->createImageFromMemoryRGBA(width, height, rgba); +} + +void Canvas::updateImageRGBA(ImageHandle image, const unsigned char* rgba) { + if (image.isValid()) { + mBackend->updateImageRGBA(image, rgba); + } +} + +Vec2F Canvas::imageSize(ImageHandle image) { return image.isValid() ? mBackend->imageSize(image) : Vec2F(0, 0); } + +void Canvas::deleteImage(ImageHandle image) { + if (image.isValid()) { + mBackend->deleteImage(image); } - widths[length] = total; } void Canvas::drawImage(RectF rect, ImageHandle image, halnf angle, halnf alpha, halnf rounding) { - if (!image.isValid()) { - return; + if (image.isValid()) { + mBackend->drawImage(placed(rect), image, angle, alpha, rounding); } - rect.pos += mOrigin; - const NVGpaint paint = nvgImagePattern(mContext->vg, rect.x, rect.y, rect.z, rect.w, angle, image.mId, alpha); - nvgBeginPath(mContext->vg); - nvgRoundedRect(mContext->vg, rect.x, rect.y, rect.z, rect.w, rounding); - nvgFillPaint(mContext->vg, paint); - nvgFill(mContext->vg); } diff --git a/GraphicApplication/src/canvas/private/CanvasContext.hpp b/GraphicApplication/src/canvas/private/CanvasContext.hpp deleted file mode 100644 index 5dc527b9..00000000 --- a/GraphicApplication/src/canvas/private/CanvasContext.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "Canvas.hpp" -#include "CanvasFontLoader.hpp" - -#include - -namespace tp { - - class Canvas::Context { - public: - NVGcontext* vg{}; - WindowSurface* surface{}; - Profiler* profiler{}; - uint32_t flushSection = 0; - CanvasFontIds fonts; - - // NanoVG reads the recording command buffer and frame slot through these pointers on every flush. - VkCommandBuffer frameCommandBuffers[WindowSurface::kFramesInFlight] = {}; - uint32_t frameSlot = 0; - }; - - inline NVGcolor toNVGColor(const RGBA& color) { return { color.r, color.g, color.b, color.a }; } -} diff --git a/GraphicApplication/src/canvas/private/CanvasImages.cpp b/GraphicApplication/src/canvas/private/CanvasImages.cpp deleted file mode 100644 index f5e88bc6..00000000 --- a/GraphicApplication/src/canvas/private/CanvasImages.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "CanvasContext.hpp" - -#include "RenderTargetRegistry.hpp" - -#include - -using namespace tp; - -ImageHandle Canvas::createImageFromRenderTarget(uint32_t renderTargetId) { - RenderTargetEntry entry; - if (!mContext->surface->gpu().renderTargets().resolve(renderTargetId, entry)) { - return {}; - } - const int image = nvgCreateImageFromHandleVk( - mContext->vg, - entry.image, - entry.view, - entry.sampler, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - entry.width, - entry.height, - 0 - ); - return ImageHandle(image); -} - -void Canvas::updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId) { - RenderTargetEntry entry; - if (!image.isValid() || !mContext->surface->gpu().renderTargets().resolve(renderTargetId, entry)) { - return; - } - nvgUpdateImageFromHandleVk( - mContext->vg, - image.mId, - entry.image, - entry.view, - entry.sampler, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - entry.width, - entry.height - ); -} - -ImageHandle Canvas::createImageFromFile(const char* path) { return ImageHandle(nvgCreateImage(mContext->vg, path, 0)); } - -ImageHandle Canvas::createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba) { - return ImageHandle(nvgCreateImageRGBA(mContext->vg, width, height, 0, rgba)); -} - -void Canvas::updateImageRGBA(ImageHandle image, const unsigned char* rgba) { - if (image.isValid()) { - nvgUpdateImage(mContext->vg, image.mId, rgba); - } -} - -Vec2F Canvas::imageSize(ImageHandle image) { - if (!image.isValid()) { - return { 0, 0 }; - } - int width = 0; - int height = 0; - nvgImageSize(mContext->vg, image.mId, &width, &height); - return { (halnf) width, (halnf) height }; -} - -void Canvas::deleteImage(ImageHandle image) { - if (image.isValid()) { - nvgDeleteImage(mContext->vg, image.mId); - } -} diff --git a/GraphicApplication/src/canvas/public/Canvas.hpp b/GraphicApplication/src/canvas/public/Canvas.hpp index 00110671..7e746244 100644 --- a/GraphicApplication/src/canvas/public/Canvas.hpp +++ b/GraphicApplication/src/canvas/public/Canvas.hpp @@ -1,9 +1,6 @@ #pragma once -#include "Color.hpp" -#include "Rect.hpp" -#include "WindowFrame.hpp" -#include "WindowSurface.hpp" +#include "CanvasBackend.hpp" #include "Buffer.hpp" @@ -13,77 +10,39 @@ namespace tp { class Profiler; + class NanoVgCanvasBackend; + class WindowSurface; + class WindowFrame; + struct GpuFrame; - enum class HorizontalAlign { Left, Center, Right }; - - enum class VerticalAlign { Top, Center, Bottom }; - - struct TextAlign { - HorizontalAlign horizontal = HorizontalAlign::Left; - VerticalAlign vertical = VerticalAlign::Center; - - static constexpr TextAlign leftCenter() { return { HorizontalAlign::Left, VerticalAlign::Center }; } - static constexpr TextAlign centered() { return { HorizontalAlign::Center, VerticalAlign::Center }; } - }; - - enum class FontFace { Regular, Bold, Italic, BoldItalic, Monospace }; - - struct TextStyle { - halnf size = 16; - RGBA color = RGBA(1, 1, 1, 1); - TextAlign align; - halnf padding = 0; - FontFace face = FontFace::Regular; - }; - - class ImageHandle { + // 2D vector drawing over a backend: NanoVG on a window surface, or a recording backend for headless tests. + // Coordinates are relative to the current placement origin, which starts at the content origin; translations and + // clips are scoped. + class Canvas : public TextMetrics { public: - ImageHandle() = default; + using CompositeOperation = CanvasBackend::CompositeOperation; + using PathWinding = CanvasBackend::PathWinding; + using LinearGradient = CanvasBackend::LinearGradient; + using BoxGradient = CanvasBackend::BoxGradient; - [[nodiscard]] bool isValid() const { return mId > 0; } - [[nodiscard]] bool operator==(const ImageHandle& other) const = default; - - private: - friend class Canvas; - explicit ImageHandle(int id) : - mId(id) {} - - int mId = 0; - }; - - // 2D vector drawing for one window surface. Coordinates are relative to the current placement origin, which - // starts at the content origin; placements and clips are scoped. - class Canvas { - class Context; - - public: + // A canvas drawing through the given backend; beginFrame() starts a frame. + explicit Canvas(std::unique_ptr backend); + // A canvas drawing onto a window surface; defined with the NanoVG backend, which is the only place that may know + // the GPU frame. Frame boundaries are beginFrame(frame, windowFrame) and endFrame(). Canvas(WindowSurface& surface, Profiler& profiler, const std::filesystem::path& resourceDirectory); - ~Canvas(); + ~Canvas() override; Canvas(const Canvas&) = delete; Canvas& operator=(const Canvas&) = delete; - void drawBegin(const GpuFrame& frame, const WindowFrame& windowFrame); - void drawEnd(); + // Resets origin, clips and composite operation for a new frame. + void beginFrame(); + // Starts a frame of the window surface this canvas draws onto, then resets the canvas state. + void beginFrame(const GpuFrame& frame, const WindowFrame& windowFrame); + // Finishes the surface frame. + void endFrame(); - enum class CompositeOperation { SourceOver, DestinationOut, DestinationOver }; - - enum class PathWinding { Solid, Hole }; - - struct LinearGradient { - Vec2F from; - Vec2F to; - RGBA fromColor; - RGBA toColor; - }; - - struct BoxGradient { - RectF box; - halnf radius = 0; - halnf feather = 0; - RGBA innerColor; - RGBA outerColor; - }; + [[nodiscard]] CanvasBackend& backend() { return *mBackend; } // Restores origin, clipping and composite operation when the scope ends. class SavedState { @@ -111,6 +70,15 @@ namespace tp { } }; + // Intersects the clip with an area relative to the current origin until destroyed. + class Clip : public SavedState { + public: + Clip(Canvas& canvas, const RectF& area) : + SavedState(canvas) { + mCanvas.clipTo(area); + } + }; + // Places the origin at a point relative to the current origin and clips to an area there, until destroyed. class Placement : public SavedState { public: @@ -134,25 +102,25 @@ namespace tp { void fillPath(const BoxGradient& gradient); void rect(RectF rect, const RGBA& color, halnf round = 0); - void gradientRect(RectF rect, const RGBA& from, const RGBA& to, bool horizontal, halnf round = 0); - void frame(RectF rect, const RGBA& color); - void debugCross(RectF rect, const RGBA& color); - void outline(RectF rect, const RGBA& color, halnf round, halnf width); + void gradientRect(RectF rect, const RGBA& from, const RGBA& to, Axis axis, halnf round = 0); + void outline(RectF rect, const RGBA& color, halnf round = 0, halnf width = 1); void circle(Vec2F center, halnf radius, const RGBA& color); - void circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width); - void line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness); - void polyline(const Vec2F* points, int count, const RGBA& color, halnf width); - void polygon(const Vec2F* points, int count, const RGBA& color); - void polygon(const Vec2F* points, int count, const LinearGradient& gradient); + void circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width = 1); + void line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness = 1); + void polyline(const std::vector& points, const RGBA& color, halnf width = 1); + void polygon(const std::vector& points, const RGBA& color); + void polygon(const std::vector& points, const LinearGradient& gradient); void ringSegment( Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient ); void radialGlow(Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor); - void text(const char* text, const RectF& area, const TextStyle& style); - void rotatedText(const char* text, Vec2F center, halnf size, halnf angle, const RGBA& color); - [[nodiscard]] halnf textWidth(const char* text, halnf size, FontFace face = FontFace::Regular); - void prefixWidths(const char* text, size_t length, halnf size, FontFace face, halnf* widths); + void text(const std::string& text, const RectF& area, const TextStyle& style); + void rotatedText(const std::string& text, Vec2F center, halnf size, halnf angle, const RGBA& color); + + [[nodiscard]] halnf + textWidth(const std::string& text, halnf size, FontFace face = FontFace::Regular) const override; + [[nodiscard]] std::vector prefixWidths(const std::string& text, halnf size, FontFace face) const override; [[nodiscard]] ImageHandle createImageFromRenderTarget(uint32_t renderTargetId); void updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId); @@ -167,6 +135,7 @@ namespace tp { struct SavedFrame { Vec2F origin; size_t clipDepth = 0; + CompositeOperation composite = CompositeOperation::SourceOver; }; void save(); @@ -174,10 +143,15 @@ namespace tp { void translateOrigin(Vec2F offset) { mOrigin += offset; } void clipTo(const RectF& rect); void applyClip(); + [[nodiscard]] RectF placed(const RectF& rect) const { return { rect.pos + mOrigin, rect.size }; } + [[nodiscard]] std::vector placed(const std::vector& points) const; + [[nodiscard]] LinearGradient placed(const LinearGradient& gradient) const; - std::unique_ptr mContext; + std::unique_ptr mBackend; + NanoVgCanvasBackend* mSurfaceBackend = nullptr; Buffer mClips; Buffer mSavedFrames; Vec2F mOrigin = { 0, 0 }; + CompositeOperation mComposite = CompositeOperation::SourceOver; }; } diff --git a/GraphicApplication/src/input_layer/private/InputState.cpp b/GraphicApplication/src/input_layer/private/InputState.cpp index 51e052c8..b0fc0d58 100644 --- a/GraphicApplication/src/input_layer/private/InputState.cpp +++ b/GraphicApplication/src/input_layer/private/InputState.cpp @@ -67,13 +67,23 @@ void InputState::apply(const InputEvent& event) { [this](const KeyChanged& changed) { ButtonState& state = mKeys[(size_t) changed.key]; switch (changed.action) { - case KeyAction::Press: press(state); break; - case KeyAction::Release: release(state); break; + case KeyAction::Press: + press(state); + mModifiers.press(changed.key); + break; + case KeyAction::Release: + release(state); + mModifiers.release(changed.key); + break; case KeyAction::Repeat: break; } }, [](const TextEntered&) {}, - [](const KeyboardFocusChanged&) {}, + [this](const KeyboardFocusChanged& focus) { + if (!focus.focused) { + mModifiers.releaseAll(); + } + }, [](const GestureChanged&) {}, }, event diff --git a/GraphicApplication/src/input_layer/private/KeyModifiers.cpp b/GraphicApplication/src/input_layer/private/KeyModifiers.cpp new file mode 100644 index 00000000..8851d41d --- /dev/null +++ b/GraphicApplication/src/input_layer/private/KeyModifiers.cpp @@ -0,0 +1,56 @@ +#include "KeyModifiers.hpp" + +using namespace tp; + +bool tp::isModifierKey(Key key) { + switch (key) { + case Key::LeftShift: + case Key::RightShift: + case Key::LeftControl: + case Key::RightControl: + case Key::LeftAlt: + case Key::RightAlt: + case Key::LeftSuper: + case Key::RightSuper: return true; + default: return false; + } +} + +int ModifierTracker::slotOf(Key key) { + switch (key) { + case Key::LeftShift: return LeftShift; + case Key::RightShift: return RightShift; + case Key::LeftControl: return LeftControl; + case Key::RightControl: return RightControl; + case Key::LeftAlt: return LeftAlt; + case Key::RightAlt: return RightAlt; + case Key::LeftSuper: return LeftSuper; + case Key::RightSuper: return RightSuper; + default: return -1; + } +} + +void ModifierTracker::press(Key key) { + const int slot = slotOf(key); + if (slot >= 0) { + mDown.set((size_t) slot); + } +} + +void ModifierTracker::release(Key key) { + const int slot = slotOf(key); + if (slot >= 0) { + mDown.reset((size_t) slot); + } +} + +bool ModifierTracker::isDown(Key key) const { return mDown.test((size_t) slotOf(key)); } + +KeyModifiers ModifierTracker::current() const { + KeyModifiers modifiers; + modifiers.shift = isDown(Key::LeftShift) || isDown(Key::RightShift); + modifiers.control = isDown(Key::LeftControl) || isDown(Key::RightControl); + modifiers.alt = isDown(Key::LeftAlt) || isDown(Key::RightAlt); + modifiers.super = isDown(Key::LeftSuper) || isDown(Key::RightSuper); + return modifiers; +} diff --git a/GraphicApplication/src/input_layer/public/InputState.hpp b/GraphicApplication/src/input_layer/public/InputState.hpp index e58f6310..9894c666 100644 --- a/GraphicApplication/src/input_layer/public/InputState.hpp +++ b/GraphicApplication/src/input_layer/public/InputState.hpp @@ -1,6 +1,7 @@ #pragma once #include "InputSink.hpp" +#include "KeyModifiers.hpp" #include #include @@ -30,6 +31,9 @@ namespace tp { [[nodiscard]] const ButtonState& button(PointerButton button) const { return mPointerButtons[(size_t) button]; } [[nodiscard]] const ButtonState& key(Key key) const { return mKeys[(size_t) key]; } + // Modifiers held at the end of the frame; walk events() with a ModifierTracker for per-event modifiers. + [[nodiscard]] KeyModifiers modifiers() const { return mModifiers.current(); } + [[nodiscard]] Vec2F scroll() const { return mScrollDelta; } [[nodiscard]] bool isContinuousScroll() const { return mContinuousScroll; } [[nodiscard]] bool isScrollEnded() const { return mScrollEnded; } @@ -52,6 +56,7 @@ namespace tp { std::array mPointerButtons{}; std::array mKeys{}; + ModifierTracker mModifiers; }; // Collects delivered events between frames and releases held buttons and keys when focus or the pointer is lost. diff --git a/GraphicApplication/src/input_layer/public/KeyModifiers.hpp b/GraphicApplication/src/input_layer/public/KeyModifiers.hpp new file mode 100644 index 00000000..eca98f91 --- /dev/null +++ b/GraphicApplication/src/input_layer/public/KeyModifiers.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "InputCodes.hpp" + +#include + +namespace tp { + + // Which modifier keys are held; either side of the keyboard counts. + struct KeyModifiers { + bool shift = false; + bool control = false; + bool alt = false; + bool super = false; + + [[nodiscard]] bool none() const { return !shift && !control && !alt && !super; } + [[nodiscard]] bool operator==(const KeyModifiers& other) const = default; + + [[nodiscard]] static KeyModifiers shiftOnly() { return { true, false, false, false }; } + [[nodiscard]] static KeyModifiers controlOnly() { return { false, true, false, false }; } + [[nodiscard]] static KeyModifiers altOnly() { return { false, false, true, false }; } + }; + + [[nodiscard]] bool isModifierKey(Key key); + + // Follows modifier key presses and releases in event order, so every event can be read with the modifiers held at + // that moment rather than at the end of the frame. + class ModifierTracker { + public: + void press(Key key); + void release(Key key); + void releaseAll() { mDown.reset(); } + + [[nodiscard]] KeyModifiers current() const; + + private: + [[nodiscard]] bool isDown(Key key) const; + + enum Slot { LeftShift, RightShift, LeftControl, RightControl, LeftAlt, RightAlt, LeftSuper, RightSuper, SlotCount }; + + [[nodiscard]] static int slotOf(Key key); + + std::bitset mDown; + }; +} diff --git a/GraphicApplication/src/backends/contract/private/PlatformInfo.cpp b/GraphicApplication/src/window_types/private/PlatformInfo.cpp similarity index 100% rename from GraphicApplication/src/backends/contract/private/PlatformInfo.cpp rename to GraphicApplication/src/window_types/private/PlatformInfo.cpp diff --git a/GraphicApplication/src/window_types/private/WindowTypes.cpp b/GraphicApplication/src/window_types/private/WindowTypes.cpp new file mode 100644 index 00000000..a5fe25f0 --- /dev/null +++ b/GraphicApplication/src/window_types/private/WindowTypes.cpp @@ -0,0 +1,18 @@ +#include "WindowTypes.hpp" + +using namespace tp; + +CursorShape tp::cursorForEdge(WindowEdge edge) { + switch (edge) { + case WindowEdge::Left: + case WindowEdge::Right: return CursorShape::ResizeHorizontal; + case WindowEdge::Top: + case WindowEdge::Bottom: return CursorShape::ResizeVertical; + case WindowEdge::TopLeft: + case WindowEdge::BottomRight: return CursorShape::ResizeNorthWestSouthEast; + case WindowEdge::TopRight: + case WindowEdge::BottomLeft: return CursorShape::ResizeNorthEastSouthWest; + case WindowEdge::None: return CursorShape::Default; + } + return CursorShape::Default; +} diff --git a/GraphicApplication/src/backends/contract/public/PlatformInfo.hpp b/GraphicApplication/src/window_types/public/PlatformInfo.hpp similarity index 100% rename from GraphicApplication/src/backends/contract/public/PlatformInfo.hpp rename to GraphicApplication/src/window_types/public/PlatformInfo.hpp diff --git a/GraphicApplication/src/backends/contract/public/WindowTypes.hpp b/GraphicApplication/src/window_types/public/WindowTypes.hpp similarity index 100% rename from GraphicApplication/src/backends/contract/public/WindowTypes.hpp rename to GraphicApplication/src/window_types/public/WindowTypes.hpp diff --git a/GraphicApplication/tests/CanvasTests.cpp b/GraphicApplication/tests/CanvasTests.cpp new file mode 100644 index 00000000..387779fc --- /dev/null +++ b/GraphicApplication/tests/CanvasTests.cpp @@ -0,0 +1,201 @@ +#include "Canvas.hpp" +#include "RecordingCanvasBackend.hpp" + +#include "UnitTest++/UnitTest++.h" + +#include + +using namespace tp; + +namespace { + constexpr halnf kTolerance = 0.001f; + + struct RecordingScene { + RecordingCanvasBackend* backend = nullptr; + std::unique_ptr canvas; + + RecordingScene() { + auto recording = std::make_unique(); + backend = recording.get(); + canvas = std::make_unique(std::move(recording)); + canvas->beginFrame(); + } + + [[nodiscard]] const CanvasCommand& last() const { return backend->commands().back(); } + }; + + void checkRect(const RectF& expected, const RectF& actual) { + CHECK_CLOSE(expected.x, actual.x, kTolerance); + CHECK_CLOSE(expected.y, actual.y, kTolerance); + CHECK_CLOSE(expected.z, actual.z, kTolerance); + CHECK_CLOSE(expected.w, actual.w, kTolerance); + } +} + +SUITE(Canvas) { + TEST(DrawingIsPlacedAtTheTranslatedOrigin) { + RecordingScene scene; + { + const Canvas::Translation translation(*scene.canvas, { 10, 20 }); + scene.canvas->rect({ 1, 2, 3, 4 }, RGBA(1, 0, 0, 1)); + } + checkRect({ 11, 22, 3, 4 }, scene.last().rect); + + scene.canvas->rect({ 1, 2, 3, 4 }, RGBA(1, 0, 0, 1)); + checkRect({ 1, 2, 3, 4 }, scene.last().rect); + } + + TEST(ClipReachesTheBackendAndIntersectsNestedClips) { + RecordingScene scene; + { + const Canvas::Clip outer(*scene.canvas, { 0, 0, 100, 50 }); + const Canvas::Translation translation(*scene.canvas, { 80, 10 }); + const Canvas::Clip inner(*scene.canvas, { 0, 0, 40, 100 }); + scene.canvas->rect({ 0, 0, 10, 10 }, RGBA(1, 1, 1, 1)); + } + + CHECK(scene.last().clipped); + checkRect({ 80, 10, 20, 40 }, scene.last().clip); + } + + TEST(ClosingAClipScopeRestoresTheOuterClip) { + RecordingScene scene; + const Canvas::Clip outer(*scene.canvas, { 0, 0, 100, 100 }); + { + const Canvas::Clip inner(*scene.canvas, { 10, 10, 20, 20 }); + } + scene.canvas->rect({ 0, 0, 1, 1 }, RGBA(1, 1, 1, 1)); + + checkRect({ 0, 0, 100, 100 }, scene.last().clip); + } + + TEST(DrawingAfterAllScopesCloseIsUnclipped) { + RecordingScene scene; + { + const Canvas::Clip clip(*scene.canvas, { 0, 0, 5, 5 }); + } + scene.canvas->rect({ 0, 0, 1, 1 }, RGBA(1, 1, 1, 1)); + + CHECK(!scene.last().clipped); + } + + TEST(OutsideClipIsReportedInLocalCoordinates) { + RecordingScene scene; + const Canvas::Clip clip(*scene.canvas, { 0, 0, 50, 50 }); + const Canvas::Translation translation(*scene.canvas, { 40, 0 }); + + CHECK(!scene.canvas->isOutsideClip({ 0, 0, 20, 20 })); + CHECK(scene.canvas->isOutsideClip({ 20, 0, 20, 20 })); + } + + TEST(TextOutsideTheClipIsNotDrawn) { + RecordingScene scene; + const Canvas::Clip clip(*scene.canvas, { 0, 0, 50, 50 }); + scene.canvas->text("hidden", { 100, 100, 40, 20 }, TextStyle{}); + + CHECK(!scene.backend->hasText("hidden")); + } + + TEST(TextIsClippedToItsPaddedArea) { + RecordingScene scene; + TextStyle style; + style.padding = 2; + scene.canvas->text("label", { 10, 10, 40, 20 }, style); + + const CanvasCommand* command = scene.backend->findText("label"); + REQUIRE CHECK(command); + checkRect({ 12, 12, 36, 16 }, command->rect); + CHECK(command->clipped); + checkRect({ 12, 12, 36, 16 }, command->clip); + } + + TEST(SavedStateRestoresCompositeOperation) { + RecordingScene scene; + { + const Canvas::SavedState saved(*scene.canvas); + scene.canvas->setCompositeOperation(Canvas::CompositeOperation::DestinationOut); + scene.canvas->beginPath(); + scene.canvas->addPathRect({ 0, 0, 1, 1 }); + scene.canvas->fillPath(RGBA(0, 0, 0, 1)); + } + CHECK(scene.last().composite == Canvas::CompositeOperation::DestinationOut); + + scene.canvas->rect({ 0, 0, 1, 1 }, RGBA(1, 1, 1, 1)); + CHECK(scene.last().composite == Canvas::CompositeOperation::SourceOver); + } + + TEST(PointsAndGradientsFollowTheOrigin) { + RecordingScene scene; + const Canvas::Translation translation(*scene.canvas, { 5, 5 }); + scene.canvas->polyline({ { 0, 0 }, { 10, 0 } }, RGBA(1, 1, 1, 1), 2); + + REQUIRE CHECK_EQUAL(2u, scene.last().points.size()); + CHECK_CLOSE(5, scene.last().points[0].x, kTolerance); + CHECK_CLOSE(15, scene.last().points[1].x, kTolerance); + CHECK_CLOSE(2, scene.last().width, kTolerance); + } + + TEST(GradientTakesAnAxis) { + RecordingScene scene; + scene.canvas->gradientRect({ 0, 0, 10, 10 }, RGBA(0, 0, 0, 1), RGBA(1, 1, 1, 1), Axis::Vertical); + + CHECK(scene.last().kind == CanvasCommand::Kind::Gradient); + CHECK_CLOSE(1, scene.last().endColor.r, kTolerance); + } + + TEST(InvalidImagesAreIgnored) { + RecordingScene scene; + scene.canvas->drawImage({ 0, 0, 1, 1 }, ImageHandle()); + scene.canvas->deleteImage(ImageHandle()); + + CHECK(scene.backend->commands().empty()); + CHECK_CLOSE(0, scene.canvas->imageSize(ImageHandle()).x, kTolerance); + } + + TEST(ImagesLiveUntilDeleted) { + RecordingScene scene; + const unsigned char pixel[4] = { 255, 255, 255, 255 }; + const ImageHandle image = scene.canvas->createImageFromMemoryRGBA(3, 2, pixel); + + REQUIRE CHECK(image.isValid()); + CHECK_CLOSE(3, scene.canvas->imageSize(image).x, kTolerance); + CHECK_EQUAL(1u, scene.backend->liveImageCount()); + + scene.canvas->deleteImage(image); + CHECK_EQUAL(0u, scene.backend->liveImageCount()); + } + + TEST(ImageFilesLoadOnlyWhenKnown) { + RecordingScene scene; + scene.backend->addImageFile("icon.png", { 16, 16 }); + + CHECK(scene.canvas->createImageFromFile("icon.png").isValid()); + CHECK(!scene.canvas->createImageFromFile("missing.png").isValid()); + } + + TEST(TextMeasurementIsAvailableWithoutAFrame) { + RecordingScene scene; + const TextMetrics& metrics = *scene.canvas; + + CHECK_CLOSE( + 4 * 10 * RecordingCanvasBackend::kCharacterWidthFactor, + metrics.textWidth("abcd", 10, FontFace::Regular), + kTolerance + ); + + const std::vector prefixes = metrics.prefixWidths("ab", 10, FontFace::Regular); + REQUIRE CHECK_EQUAL(3u, prefixes.size()); + CHECK_CLOSE(0, prefixes[0], kTolerance); + CHECK_CLOSE(10, prefixes[2], kTolerance); + } + + TEST(BeginFrameResetsCompositeOperation) { + RecordingScene scene; + scene.canvas->setCompositeOperation(Canvas::CompositeOperation::DestinationOver); + scene.canvas->beginFrame(); + scene.canvas->rect({ 1, 1, 1, 1 }, RGBA(1, 1, 1, 1)); + + CHECK(scene.last().composite == Canvas::CompositeOperation::SourceOver); + CHECK(!scene.last().clipped); + } +} diff --git a/GraphicApplication/tests/InputStateTests.cpp b/GraphicApplication/tests/InputStateTests.cpp index 8868a05c..1c5ffd16 100644 --- a/GraphicApplication/tests/InputStateTests.cpp +++ b/GraphicApplication/tests/InputStateTests.cpp @@ -126,3 +126,55 @@ SUITE(InputState) { CHECK_CLOSE(30, hidden.relativeTo({ 0, 0 }).getPointer().x, 0.0001f); } } + +SUITE(KeyModifiers) { + TEST(EitherSideOfTheKeyboardCounts) { + ModifierTracker tracker; + tracker.press(Key::RightControl); + CHECK(tracker.current().control); + + tracker.press(Key::LeftControl); + tracker.release(Key::RightControl); + CHECK(tracker.current().control); + + tracker.release(Key::LeftControl); + CHECK(tracker.current().none()); + } + + TEST(OtherKeysAreNotModifiers) { + ModifierTracker tracker; + tracker.press(Key::A); + CHECK(tracker.current().none()); + CHECK(!isModifierKey(Key::A)); + CHECK(isModifierKey(Key::RightSuper)); + } + + TEST(StateHoldsModifiersAtTheEndOfTheFrame) { + InputQueue queue; + queue.post(KeyChanged{ Key::LeftShift, KeyAction::Press }); + queue.post(KeyChanged{ Key::LeftAlt, KeyAction::Press }); + queue.post(KeyChanged{ Key::LeftAlt, KeyAction::Release }); + queue.applyPendingEvents(); + + CHECK(queue.state().modifiers() == KeyModifiers::shiftOnly()); + } + + TEST(ModifiersPersistAcrossFrames) { + InputQueue queue; + queue.post(KeyChanged{ Key::LeftControl, KeyAction::Press }); + queue.applyPendingEvents(); + queue.applyPendingEvents(); + + CHECK(queue.state().modifiers().control); + } + + TEST(LosingFocusReleasesModifiers) { + InputQueue queue; + queue.post(KeyChanged{ Key::LeftControl, KeyAction::Press }); + queue.applyPendingEvents(); + queue.post(KeyboardFocusChanged{ false }); + queue.applyPendingEvents(); + + CHECK(queue.state().modifiers().none()); + } +}