GraphicApplication: canvas facade with a contract and backends, modifier keys, window types #14

Merged
ilusha merged 3 commits from GraphicApplicationCanvas into master 2026-10-30 11:02:54 +03:00
27 changed files with 1599 additions and 576 deletions

View file

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

View file

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

View file

@ -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 };

View file

@ -0,0 +1,71 @@
#pragma once
#include "CanvasTypes.hpp"
#include <cstdint>
#include <optional>
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<RectF> 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); }
};
}

View file

@ -0,0 +1,66 @@
#pragma once
#include "Color.hpp"
#include "Rect.hpp"
#include <string>
#include <vector>
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<halnf> prefixWidths(const std::string& text, halnf size, FontFace face) const = 0;
};
}

View file

@ -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<NanoVgCanvasBackend>(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();
}

View file

@ -0,0 +1,407 @@
#include "NanoVgCanvasBackend.hpp"
#include "GpuError.hpp"
#include "RenderTargetRegistry.hpp"
#include "Profiler.hpp"
#include <nanovg_vk.h>
#include <vector>
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<RectF> 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<halnf> NanoVgCanvasBackend::prefixWidths(const std::string& string, halnf size, FontFace face) const {
const size_t length = string.size();
std::vector<halnf> 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<NVGglyphPosition> 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()); }

View file

@ -0,0 +1,81 @@
#pragma once
#include "CanvasBackend.hpp"
#include "CanvasFontLoader.hpp"
#include "WindowFrame.hpp"
#include "WindowSurface.hpp"
#include <nanovg.h>
#include <filesystem>
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<RectF> 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<halnf> 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 }; }
}

View file

@ -0,0 +1,211 @@
#include "RecordingCanvasBackend.hpp"
#include <algorithm>
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<RectF> 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<halnf> RecordingCanvasBackend::prefixWidths(const std::string& text, halnf size, FontFace) const {
mTextMeasurements++;
std::vector<halnf> 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; });
}

View file

@ -0,0 +1,127 @@
#pragma once
#include "CanvasBackend.hpp"
#include <optional>
#include <string>
#include <vector>
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<Vec2F> 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<RectF> 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<halnf> 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<CanvasCommand>& 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<CanvasCommand> mCommands;
std::vector<Image> mImages;
std::vector<ImageFile> mImageFiles;
std::vector<RectF> mPathRects;
std::optional<RectF> mClip;
CompositeOperation mComposite = CompositeOperation::SourceOver;
mutable size_t mTextMeasurements = 0;
};
}

View file

@ -1,110 +1,22 @@
#include "CanvasContext.hpp"
#include "GpuError.hpp"
#include "Profiler.hpp"
#include <nanovg_vk.h>
#include <vector>
#include "Canvas.hpp"
using namespace tp;
namespace {
constexpr halnf kItalicSkew = -0.21f;
Canvas::Canvas(std::unique_ptr<CanvasBackend> 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<Context>()) {
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<RectF>(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<Vec2F> Canvas::placed(const std::vector<Vec2F>& points) const {
std::vector<Vec2F> 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<Vec2F>& points, const RGBA& color, halnf width) {
const std::vector<Vec2F> placedPoints = placed(points);
mBackend->polyline(placedPoints.data(), (int) placedPoints.size(), color, width);
}
void Canvas::polygon(const std::vector<Vec2F>& points, const RGBA& color) {
const std::vector<Vec2F> placedPoints = placed(points);
mBackend->polygon(placedPoints.data(), (int) placedPoints.size(), color);
}
void Canvas::polygon(const std::vector<Vec2F>& points, const LinearGradient& gradient) {
const std::vector<Vec2F> 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<halnf> Canvas::prefixWidths(const std::string& string, halnf size, FontFace face) const {
return mBackend->prefixWidths(string, size, face);
}
std::vector<NVGglyphPosition> 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);
}

View file

@ -1,24 +0,0 @@
#pragma once
#include "Canvas.hpp"
#include "CanvasFontLoader.hpp"
#include <nanovg.h>
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 }; }
}

View file

@ -1,70 +0,0 @@
#include "CanvasContext.hpp"
#include "RenderTargetRegistry.hpp"
#include <nanovg_vk.h>
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);
}
}

View file

@ -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<CanvasBackend> 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<Vec2F>& points, const RGBA& color, halnf width = 1);
void polygon(const std::vector<Vec2F>& points, const RGBA& color);
void polygon(const std::vector<Vec2F>& 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<halnf> 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<Vec2F> placed(const std::vector<Vec2F>& points) const;
[[nodiscard]] LinearGradient placed(const LinearGradient& gradient) const;
std::unique_ptr<Context> mContext;
std::unique_ptr<CanvasBackend> mBackend;
NanoVgCanvasBackend* mSurfaceBackend = nullptr;
Buffer<RectF> mClips;
Buffer<SavedFrame> mSavedFrames;
Vec2F mOrigin = { 0, 0 };
CompositeOperation mComposite = CompositeOperation::SourceOver;
};
}

View file

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

View file

@ -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;
}

View file

@ -1,6 +1,7 @@
#pragma once
#include "InputSink.hpp"
#include "KeyModifiers.hpp"
#include <array>
#include <cstddef>
@ -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<ButtonState, kPointerButtonCount> mPointerButtons{};
std::array<ButtonState, kKeyCodeLimit> mKeys{};
ModifierTracker mModifiers;
};
// Collects delivered events between frames and releases held buttons and keys when focus or the pointer is lost.

View file

@ -0,0 +1,45 @@
#pragma once
#include "InputCodes.hpp"
#include <bitset>
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<SlotCount> mDown;
};
}

View file

@ -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;
}

View file

@ -0,0 +1,201 @@
#include "Canvas.hpp"
#include "RecordingCanvasBackend.hpp"
#include "UnitTest++/UnitTest++.h"
#include <memory>
using namespace tp;
namespace {
constexpr halnf kTolerance = 0.001f;
struct RecordingScene {
RecordingCanvasBackend* backend = nullptr;
std::unique_ptr<Canvas> canvas;
RecordingScene() {
auto recording = std::make_unique<RecordingCanvasBackend>();
backend = recording.get();
canvas = std::make_unique<Canvas>(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<halnf> 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);
}
}

View file

@ -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());
}
}