GraphicApplication: canvas facade with a contract and backends, modifier keys, window types #14
27 changed files with 1599 additions and 576 deletions
|
|
@ -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 ()
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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()); }
|
||||
|
|
@ -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 }; }
|
||||
}
|
||||
|
|
@ -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; });
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
56
GraphicApplication/src/input_layer/private/KeyModifiers.cpp
Normal file
56
GraphicApplication/src/input_layer/private/KeyModifiers.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
45
GraphicApplication/src/input_layer/public/KeyModifiers.hpp
Normal file
45
GraphicApplication/src/input_layer/public/KeyModifiers.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
18
GraphicApplication/src/window_types/private/WindowTypes.cpp
Normal file
18
GraphicApplication/src/window_types/private/WindowTypes.cpp
Normal 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;
|
||||
}
|
||||
201
GraphicApplication/tests/CanvasTests.cpp
Normal file
201
GraphicApplication/tests/CanvasTests.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue