UI rewrite 3rd attempt

This commit is contained in:
Jorijn van der Graaf 2026-05-02 21:08:20 +02:00
commit 1f5697326c
48 changed files with 2155 additions and 6190 deletions

View file

@ -0,0 +1,91 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#include "vulkan/vulkan.h"
module Crafter.Graphics:ComputeShader_impl;
import :ComputeShader;
import :ShaderVulkan;
import :Device;
import std;
using namespace Crafter;
ComputeShader::ComputeShader(ComputeShader&& other) noexcept : pipeline(other.pipeline) {
other.pipeline = VK_NULL_HANDLE;
}
ComputeShader& ComputeShader::operator=(ComputeShader&& other) noexcept {
if (this != &other) {
if (pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(Device::device, pipeline, nullptr);
}
pipeline = other.pipeline;
other.pipeline = VK_NULL_HANDLE;
}
return *this;
}
ComputeShader::~ComputeShader() {
if (pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(Device::device, pipeline, nullptr);
pipeline = VK_NULL_HANDLE;
}
}
void ComputeShader::Load(const std::filesystem::path& spvPath) {
VulkanShader shader(spvPath, "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr);
// Spec: with VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT, layout MUST be
// VK_NULL_HANDLE — bindings come from the bound descriptor heap and push
// constants are pushed via vkCmdPushDataEXT instead of vkCmdPushConstants.
VkPipelineCreateFlags2CreateInfo flags2 {
.sType = VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO,
.flags = VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT,
};
VkComputePipelineCreateInfo info {
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.pNext = &flags2,
.stage = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
.module = shader.shader,
.pName = "main",
},
.layout = VK_NULL_HANDLE,
};
Device::CheckVkResult(vkCreateComputePipelines(
Device::device, VK_NULL_HANDLE, 1, &info, nullptr, &pipeline));
}
void ComputeShader::Dispatch(VkCommandBuffer cmd,
const void* push, std::uint32_t pushBytes,
std::uint32_t gx,
std::uint32_t gy,
std::uint32_t gz) const {
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
if (push != nullptr && pushBytes > 0) {
VkPushDataInfoEXT pushInfo {
.sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT,
.offset = 0,
.data = { .address = const_cast<void*>(push), .size = pushBytes },
};
Device::vkCmdPushDataEXT(cmd, &pushInfo);
}
vkCmdDispatch(cmd, gx, gy, gz);
}

View file

@ -386,8 +386,10 @@ void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std:
Device::wlPointer = wl_pointer;
for(Window* window : windows) {
if(window->surface == surface) {
window->lastPointerSerial_ = serial;
if(window->cursorSurface != nullptr) {
wl_pointer_set_cursor(wl_pointer, serial, window->cursorSurface, 0, 0);
wl_pointer_set_cursor(wl_pointer, serial, window->cursorSurface,
window->cursorHotspotX_, window->cursorHotspotY_);
}
focusedWindow = window;
window->onMouseEnter.Invoke();

View file

@ -19,15 +19,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
module;
#include "vulkan/vulkan.h"
#include "../lib/stb_truetype.h"
module Crafter.Graphics:UIAtlas_impl;
import :UIAtlas;
module Crafter.Graphics:FontAtlas_impl;
import :FontAtlas;
import :Font;
import :ImageVulkan;
import :Device;
import std;
using namespace Crafter;
using namespace Crafter::UI;
void FontAtlas::Initialize(VkCommandBuffer cmd) {
image.Create(

View file

@ -0,0 +1,393 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#include "vulkan/vulkan.h"
module Crafter.Graphics:UI_impl;
import :UI;
import :ComputeShader;
import :Device;
import :Window;
import :DescriptorHeapVulkan;
import :ImageVulkan;
import :VulkanBuffer;
import :FontAtlas;
import :Font;
import std;
using namespace Crafter;
// ─── Initialize ─────────────────────────────────────────────────────────
void UIRenderer::Initialize(Window& window, DescriptorHeapVulkan& heap, VkCommandBuffer initCmd,
std::filesystem::path quadsSpv,
std::filesystem::path circlesSpv,
std::filesystem::path imagesSpv,
std::filesystem::path textSpv) {
window_ = &window;
heap_ = &heap;
// Load the four standard pipelines.
drawQuads.Load(quadsSpv);
drawCircles.Load(circlesSpv);
drawImages.Load(imagesSpv);
drawText.Load(textSpv);
// Allocate one image slot for the swapchain output. Each per-frame heap
// copy will hold ITS frame's image at this slot.
auto outRange = heap_->AllocateImageSlots(1);
outImageSlot_ = outRange.firstElement;
WriteSwapchainDescriptors();
// Optional font-atlas registration (user must have called atlas->Initialize
// already before reaching here, so atlas->image is live).
if (fontAtlas != nullptr) {
auto atlasImg = heap_->AllocateImageSlots(1);
fontAtlasImageSlot_ = atlasImg.firstElement;
fontAtlasSamplerSlot_ = RegisterLinearClampSampler();
WriteFontAtlasDescriptor();
}
// Flush the host-mapped descriptor heaps so the GPU sees what we wrote.
for (auto& h : heap_->resourceHeap) h.FlushDevice();
for (auto& h : heap_->samplerHeap) h.FlushDevice();
(void)initCmd; // reserved for future image-layout tweaks
}
// ─── per-frame Record ───────────────────────────────────────────────────
void UIRenderer::Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
// Reset per-frame state.
firstDispatchThisFrame_ = true;
// If text is in use, flush any glyphs that user-side ShapeText calls
// produced during a previous frame's onBuild. (Ensure() during the
// current onBuild also marks the atlas dirty; that's flushed on the
// NEXT Record. For v1 this is fine because the current frame's text
// dispatch reads whatever's already been uploaded, and brand-new glyphs
// missing from the atlas this frame will simply render blank for one
// frame and resolve next frame. To get them this frame, the user can
// call atlas->Update(cmd) themselves at the top of onBuild.)
if (fontAtlas != nullptr && fontAtlas->dirty) {
fontAtlas->Update(cmd);
}
onBuild.Invoke({cmd, frameIdx});
(void)window;
}
// ─── header builder ─────────────────────────────────────────────────────
UIDispatchHeader UIRenderer::FillHeader(std::uint32_t itemBufferSlot,
std::uint32_t itemCount,
std::array<float,4> clipRectPx,
std::uint32_t flags) const noexcept {
UIDispatchHeader h{};
h.outImage = outImageSlot_;
h.itemBuffer = itemBufferSlot;
h.surfaceWidth = window_->width;
h.surfaceHeight = window_->height;
h.clipX = clipRectPx[0];
h.clipY = clipRectPx[1];
h.clipW = clipRectPx[2];
h.clipH = clipRectPx[3];
h.itemCount = itemCount;
h.frameIdx = window_->currentBuffer;
h.flags = flags;
h._pad = 0;
return h;
}
// ─── group-count helper ────────────────────────────────────────────────
namespace {
// Number of 8-pixel tiles needed to cover `dim` pixels (rounded up).
inline std::uint32_t TilesFor(std::uint32_t dim) {
return (dim + 7u) / 8u;
}
}
// ─── standard-shader convenience dispatches ─────────────────────────────
//
// All four standard shaders use the same pixel-tile dispatch model: one
// workgroup per 8×8 screen tile, each thread iterates every item in order
// inside the workgroup, accumulating into a local register. This guarantees
// "items in the buffer render in order" (later items overdraw earlier ones)
// without inter-workgroup races on imageLoad/imageStore — the bug that the
// per-item dispatch model had.
void UIRenderer::DispatchQuads(VkCommandBuffer cmd, std::uint32_t bufferSlot,
std::uint32_t itemCount,
std::array<float,4> clipRectPx) {
if (itemCount == 0) return;
struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
Dispatch(cmd, drawQuads, &pc, sizeof(pc),
TilesFor(window_->width), TilesFor(window_->height), 1u);
}
void UIRenderer::DispatchCircles(VkCommandBuffer cmd, std::uint32_t bufferSlot,
std::uint32_t itemCount,
std::array<float,4> clipRectPx) {
if (itemCount == 0) return;
struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
Dispatch(cmd, drawCircles, &pc, sizeof(pc),
TilesFor(window_->width), TilesFor(window_->height), 1u);
}
void UIRenderer::DispatchImages(VkCommandBuffer cmd, std::uint32_t bufferSlot,
std::uint32_t itemCount,
std::array<float,4> clipRectPx) {
if (itemCount == 0) return;
struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
Dispatch(cmd, drawImages, &pc, sizeof(pc),
TilesFor(window_->width), TilesFor(window_->height), 1u);
}
void UIRenderer::DispatchText(VkCommandBuffer cmd, std::uint32_t bufferSlot,
std::uint32_t itemCount,
std::array<float,4> clipRectPx) {
if (itemCount == 0) return;
if (fontAtlasImageSlot_ == 0xFFFF) {
throw std::runtime_error("UIRenderer::DispatchText: no FontAtlas registered (set fontAtlas before Initialize)");
}
// Flush any glyphs that ShapeText calls (during this onBuild) just
// rasterised, so the dispatch below sees them.
if (fontAtlas != nullptr && fontAtlas->dirty) {
fontAtlas->Update(cmd);
}
struct PC {
UIDispatchHeader hdr;
std::uint32_t fontTextureSlot;
std::uint32_t fontSamplerSlot;
std::uint32_t _p0;
std::uint32_t _p1;
} pc {
FillHeader(bufferSlot, itemCount, clipRectPx),
fontAtlasImageSlot_,
fontAtlasSamplerSlot_,
0, 0
};
Dispatch(cmd, drawText, &pc, sizeof(pc),
TilesFor(window_->width), TilesFor(window_->height), 1u);
}
// ─── generic Dispatch (with barrier) ────────────────────────────────────
void UIRenderer::Dispatch(VkCommandBuffer cmd, const ComputeShader& shader,
const void* push, std::uint32_t pushBytes,
std::uint32_t gx, std::uint32_t gy, std::uint32_t gz) {
if (!firstDispatchThisFrame_) {
VkMemoryBarrier mb {
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
};
vkCmdPipelineBarrier(cmd,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0, 1, &mb, 0, nullptr, 0, nullptr);
}
firstDispatchThisFrame_ = false;
shader.Dispatch(cmd, push, pushBytes, gx, gy, gz);
}
// ─── descriptor writes ─────────────────────────────────────────────────
void UIRenderer::WriteSwapchainDescriptors() {
// Each per-frame heap holds ITS swapchain image at outImageSlot_.
std::array<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
infos[f] = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
.pView = &window_->imageViews[f],
.layout = VK_IMAGE_LAYOUT_GENERAL,
};
resources[f] = {
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.data = { .pImage = &infos[f] },
};
destinations[f] = {
.address = heap_->resourceHeap[f].value
+ heap_->ImageByteOffset(outImageSlot_),
.size = Device::descriptorHeapProperties.imageDescriptorSize,
};
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
}
void UIRenderer::WriteFontAtlasDescriptor() {
atlasViewCreateInfo_ = {
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = fontAtlas->image.image,
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = VK_FORMAT_R8_UNORM,
.components = {
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
},
.subresourceRange = {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
WriteSampledImageDescriptor(fontAtlasImageSlot_,
atlasViewCreateInfo_,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}
void UIRenderer::WriteSampledImageDescriptor(std::uint16_t slot,
const VkImageViewCreateInfo& viewInfo,
VkImageLayout layout) {
std::array<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
infos[f] = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
.pView = &viewInfo,
.layout = layout,
};
resources[f] = {
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
.data = { .pImage = &infos[f] },
};
destinations[f] = {
.address = heap_->resourceHeap[f].value + heap_->ImageByteOffset(slot),
.size = Device::descriptorHeapProperties.imageDescriptorSize,
};
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
for (auto& h : heap_->resourceHeap) h.FlushDevice();
}
void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size) {
std::array<VkDeviceAddressRangeEXT, Window::numFrames> ranges{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
ranges[f] = { .address = address, .size = size };
resources[f] = {
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.data = { .pAddressRange = &ranges[f] },
};
destinations[f] = {
.address = heap_->resourceHeap[f].value + heap_->BufferByteOffset(slot),
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
};
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
for (auto& h : heap_->resourceHeap) h.FlushDevice();
}
std::uint16_t UIRenderer::RegisterSampler(const VkSamplerCreateInfo& info) {
auto range = heap_->AllocateSamplerSlots(1);
std::array<VkSamplerCreateInfo, Window::numFrames> infos{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
infos[f] = info;
destinations[f] = {
.address = heap_->samplerHeap[f].value + heap_->SamplerByteOffset(range.firstElement),
.size = Device::descriptorHeapProperties.samplerDescriptorSize,
};
}
Device::vkWriteSamplerDescriptorsEXT(
Device::device, Window::numFrames, infos.data(), destinations.data()
);
for (auto& h : heap_->samplerHeap) h.FlushDevice();
return range.firstElement;
}
std::uint16_t UIRenderer::RegisterLinearClampSampler() {
VkSamplerCreateInfo s {
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.magFilter = VK_FILTER_LINEAR,
.minFilter = VK_FILTER_LINEAR,
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.maxAnisotropy = 1.0f,
.minLod = 0.0f,
.maxLod = VK_LOD_CLAMP_NONE,
};
return RegisterSampler(s);
}
// ─── ShapeText ─────────────────────────────────────────────────────────
std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
float x, float baselineY,
std::string_view utf8,
std::array<float,4> color,
GlyphItem* out, std::uint32_t outCapacity,
float* outAdvance) {
if (fontAtlas == nullptr) {
throw std::runtime_error("UIRenderer::ShapeText: no FontAtlas (set fontAtlas before Initialize)");
}
const float scale = pxSize / FontAtlas::kBaseSize;
float cursor = x;
std::uint32_t written = 0;
std::size_t i = 0;
while (i < utf8.size() && written < outCapacity) {
std::uint32_t cp = DecodeUtf8(utf8, i);
if (cp == 0) break;
if (cp == '\n') { /* single-line shaper — ignore */ continue; }
fontAtlas->Ensure(font, cp);
const Glyph* g = fontAtlas->Lookup(font, cp);
if (g == nullptr) continue;
// Empty glyph (whitespace) — advance only.
if (g->w > 0 && g->h > 0) {
GlyphItem& gi = out[written++];
gi.x = cursor + g->xoff * scale;
gi.y = baselineY + g->yoff * scale;
gi.w = g->w * scale;
gi.h = g->h * scale;
gi.u0 = g->u0; gi.v0 = g->v0;
gi.u1 = g->u1; gi.v1 = g->v1;
gi.r = color[0]; gi.g = color[1]; gi.b = color[2]; gi.a = color[3];
}
cursor += g->advance * scale;
}
if (outAdvance) *outAdvance = cursor - x;
return written;
}

View file

@ -0,0 +1,186 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
module Crafter.Graphics:UIComponents_impl;
import :UIComponents;
import :UI;
import :Font;
import :FontAtlas;
import std;
using namespace Crafter;
namespace {
// Push one QuadItem into buf if there's room. No-op if full.
inline void PushQuad(UIBuffer& buf, const QuadItem& q) {
if (buf.quads == nullptr || buf.quadCount == nullptr) return;
if (*buf.quadCount >= buf.quadCap) return;
buf.quads[(*buf.quadCount)++] = q;
}
inline std::array<float, 4> Pick(const std::array<float, 4>& a,
const std::array<float, 4>& b, bool useB) {
return useB ? b : a;
}
// Centered single-line text emit. Appends glyphs at the buffer's tail,
// then offsets them so the run is horizontally centered around `centerX`.
// Vertical baseline is placed at the centerY plus a coarse ascent estimate
// (fontSize * 0.32). Returns the advance width that was written.
float EmitCenteredLabel(UIBuffer& buf, std::string_view label,
Font& font, float fontSize,
float centerX, float centerY,
std::array<float, 4> color)
{
if (label.empty() || buf.atlas == nullptr || buf.renderer == nullptr) return 0.0f;
if (buf.glyphs == nullptr || buf.glyphCount == nullptr) return 0.0f;
std::uint32_t before = *buf.glyphCount;
std::uint32_t cap = (buf.glyphCap > before) ? (buf.glyphCap - before) : 0;
if (cap == 0) return 0.0f;
GlyphItem* writePos = buf.glyphs + before;
float baseline = centerY + fontSize * 0.32f;
float advance = 0.0f;
std::uint32_t n = buf.renderer->ShapeText(
font, fontSize, centerX, baseline,
label, color, writePos, cap, &advance
);
*buf.glyphCount = before + n;
// Center: shift each glyph's x by -advance/2.
float shift = -advance * 0.5f;
for (std::uint32_t i = 0; i < n; ++i) {
writePos[i].x += shift;
}
return advance;
}
}
// ─── DrawButton ─────────────────────────────────────────────────────────
void Crafter::DrawButton(UIBuffer& buf, Rect r, std::string_view label,
bool hovered, bool pressed,
Font& font, float fontSize,
const ButtonColors& c)
{
auto bg = pressed ? c.bgPressed : (hovered ? c.bgHover : c.bg);
PushQuad(buf, QuadItem{
r.x, r.y, r.w, r.h,
bg[0], bg[1], bg[2], bg[3],
c.cornerRadius, c.cornerRadius, c.cornerRadius, c.cornerRadius,
c.borderThickness, c.border[0], c.border[1], c.border[2],
});
EmitCenteredLabel(buf, label, font, fontSize,
r.x + r.w * 0.5f, r.y + r.h * 0.5f, c.text);
}
// ─── DrawCheckbox ───────────────────────────────────────────────────────
void Crafter::DrawCheckbox(UIBuffer& buf, Rect r, bool checked, bool hovered,
const CheckboxColors& c)
{
auto bg = Pick(c.bg, c.bgHover, hovered);
PushQuad(buf, QuadItem{
r.x, r.y, r.w, r.h,
bg[0], bg[1], bg[2], bg[3],
c.cornerRadius, c.cornerRadius, c.cornerRadius, c.cornerRadius,
c.borderThickness, c.border[0], c.border[1], c.border[2],
});
if (checked) {
Rect inner = r.Inset(c.checkInset);
if (inner.w > 0 && inner.h > 0) {
float innerR = std::max(0.0f, c.cornerRadius - c.checkInset);
PushQuad(buf, QuadItem{
inner.x, inner.y, inner.w, inner.h,
c.check[0], c.check[1], c.check[2], c.check[3],
innerR, innerR, innerR, innerR,
0, 0, 0, 0,
});
}
}
}
// ─── DrawSlider ─────────────────────────────────────────────────────────
void Crafter::DrawSlider(UIBuffer& buf, Rect r, float t01, bool dragging,
const SliderColors& c)
{
t01 = std::clamp(t01, 0.0f, 1.0f);
// Track is a thin centered horizontal strip.
float trackY = r.y + (r.h - c.trackHeight) * 0.5f;
float trackR = c.trackHeight * 0.5f;
float fillW = r.w * t01;
if (fillW > 0.0f) {
PushQuad(buf, QuadItem{
r.x, trackY, fillW, c.trackHeight,
c.trackFilled[0], c.trackFilled[1], c.trackFilled[2], c.trackFilled[3],
trackR, trackR, trackR, trackR,
0, 0, 0, 0,
});
}
if (fillW < r.w) {
PushQuad(buf, QuadItem{
r.x + fillW, trackY, r.w - fillW, c.trackHeight,
c.track[0], c.track[1], c.track[2], c.track[3],
trackR, trackR, trackR, trackR,
0, 0, 0, 0,
});
}
// Thumb: a quad with cornerRadius = thumbRadius (= a circle).
auto thumbColor = Pick(c.thumb, c.thumbHover, dragging);
float thumbCx = r.x + r.w * t01;
float thumbCy = r.y + r.h * 0.5f;
float d = c.thumbRadius * 2.0f;
PushQuad(buf, QuadItem{
thumbCx - c.thumbRadius, thumbCy - c.thumbRadius, d, d,
thumbColor[0], thumbColor[1], thumbColor[2], thumbColor[3],
c.thumbRadius, c.thumbRadius, c.thumbRadius, c.thumbRadius,
0, 0, 0, 0,
});
}
// ─── DrawProgressBar ────────────────────────────────────────────────────
void Crafter::DrawProgressBar(UIBuffer& buf, Rect r, float t01,
const ProgressColors& c)
{
t01 = std::clamp(t01, 0.0f, 1.0f);
PushQuad(buf, QuadItem{
r.x, r.y, r.w, r.h,
c.bg[0], c.bg[1], c.bg[2], c.bg[3],
c.cornerRadius, c.cornerRadius, c.cornerRadius, c.cornerRadius,
0, 0, 0, 0,
});
float fillW = r.w * t01;
if (fillW > 0.0f) {
PushQuad(buf, QuadItem{
r.x, r.y, fillW, r.h,
c.fill[0], c.fill[1], c.fill[2], c.fill[3],
c.cornerRadius, c.cornerRadius, c.cornerRadius, c.cornerRadius,
0, 0, 0, 0,
});
}
}

View file

@ -1,354 +0,0 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#include "vulkan/vulkan.h"
module Crafter.Graphics:UIRenderer_impl;
import :UIRenderer;
import :Device;
import :Window;
import :DescriptorHeapVulkan;
import :VulkanBuffer;
import :ShaderVulkan;
import :ImageVulkan;
import :UIDrawList;
import :UIAtlas;
import std;
using namespace Crafter;
using namespace Crafter::UI;
namespace {
// Push-constant block — must match shaders/ui.comp.glsl. The shader's
// `vec2 surfaceSize` field has 8-byte alignment under std430, so we
// insert explicit padding after `itemCount` to keep the C++ and GLSL
// layouts byte-identical (40 bytes total).
struct PC {
std::uint32_t itemCount; // 0
std::uint32_t _pad0; // 4
float surfaceSize[2]; // 8
float scale; // 16
std::uint32_t outImageHeapIdx; // 20
std::uint32_t itemBufHeapIdx; // 24
std::uint32_t atlasTextureHeapIdx; // 28
std::uint32_t bindlessBaseHeapIdx; // 32
std::uint32_t linearSamplerHeapIdx; // 36
};
static_assert(sizeof(PC) == 40, "PC layout must match shader push-constant block");
static_assert(sizeof(PC) <= 128, "Push-constant block exceeds the spec-mandated minimum (128 bytes)");
}
void UIRenderer::Initialize(Window& window,
VkCommandBuffer initCmd,
const std::filesystem::path& spvPath,
std::uint16_t bindlessImageCount) {
if (!window.descriptorHeap) {
throw std::runtime_error("UIRenderer::Initialize: window.descriptorHeap must be set first");
}
window_ = &window;
bindlessCount_ = bindlessImageCount;
auto& heap = *window.descriptorHeap;
// Slot allocation. Layout in the resource heap (image-typed indexing):
// [outImageBase_ ..] : Window::numFrames swapchain views (storage)
// [atlasImageSlot_] : 1 sampled SDF atlas
// [bindlessBase_ ..] : bindlessImageCount user image slots
auto imgSlots = heap.AllocateImageSlots(
Window::numFrames + 1 + bindlessImageCount
);
outImageBase_ = imgSlots.firstElement;
atlasImageSlot_ = imgSlots.firstElement + Window::numFrames;
bindlessBase_ = imgSlots.firstElement + Window::numFrames + 1;
// One SSBO per swapchain frame.
auto bufSlots = heap.AllocateBufferSlots(Window::numFrames);
itemBufBase_ = bufSlots.firstElement;
// One linear sampler.
auto sampSlots = heap.AllocateSamplerSlots(1);
linearSamplerSlot_ = sampSlots.firstElement;
// Initial item-buffer capacity (grows on demand).
GrowItemBuffersIfNeeded(256);
// Atlas image — Initialize records a layout transition into initCmd.
atlas.Initialize(initCmd);
CreatePipeline(spvPath);
WriteSwapchainDescriptors();
WriteAtlasDescriptor();
WriteSamplerDescriptors();
WriteItemBufferDescriptors();
for (auto& h : heap.resourceHeap) h.FlushDevice();
for (auto& h : heap.samplerHeap) h.FlushDevice();
}
void UIRenderer::GrowItemBuffersIfNeeded(std::uint32_t needed) {
if (needed <= itemCapacity_) return;
std::uint32_t newCap = itemCapacity_ ? itemCapacity_ * 2 : 256;
while (newCap < needed) newCap *= 2;
itemCapacity_ = static_cast<std::uint16_t>(std::min<std::uint32_t>(newCap, 65535));
for (auto& b : itemBufs_) {
b.Resize(
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
itemCapacity_
);
}
// Item buffer descriptors point at the buffers' device addresses, so
// they must be re-written after Resize.
if (window_) WriteItemBufferDescriptors();
}
void UIRenderer::SetItems(std::span<const UIItem> items) {
if (items.size() > itemCapacity_) {
GrowItemBuffersIfNeeded(static_cast<std::uint32_t>(items.size()));
}
pendingItemCount = static_cast<std::uint32_t>(items.size());
auto& buf = itemBufs_[window_->currentBuffer];
if (!items.empty()) {
std::memcpy(buf.value, items.data(), items.size() * sizeof(UIItem));
}
buf.FlushDevice();
}
void UIRenderer::Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
// Make sure any glyph rasterisation done during Emit lands on the GPU
// before we sample the atlas.
atlas.Update(cmd);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_);
PC pc{};
pc.itemCount = pendingItemCount;
pc.surfaceSize[0] = static_cast<float>(window.width);
pc.surfaceSize[1] = static_cast<float>(window.height);
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
pc.scale = window.scale;
#else
pc.scale = 1.0f;
#endif
pc.outImageHeapIdx = outImageBase_ + frameIdx;
// Buffer-typed shader views index the *whole* heap in buffer-descriptor
// units, so we offset past the image region: bufferStartElement is the
// first element index where buffer descriptors actually live.
pc.itemBufHeapIdx = window.descriptorHeap->bufferStartElement
+ itemBufBase_ + frameIdx;
pc.atlasTextureHeapIdx = atlasImageSlot_;
pc.bindlessBaseHeapIdx = bindlessBase_;
pc.linearSamplerHeapIdx = linearSamplerSlot_;
// Pipelines created with VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT
// use vkCmdPushDataEXT for push constants (the spec requires layout to
// be VK_NULL_HANDLE in that mode, which means vkCmdPushConstants has
// nowhere to attach to).
VkPushDataInfoEXT pushInfo{
.sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT,
.offset = 0,
.data = { .address = &pc, .size = sizeof(PC) },
};
Device::vkCmdPushDataEXT(cmd, &pushInfo);
std::uint32_t gx = (window.width + 15) / 16;
std::uint32_t gy = (window.height + 15) / 16;
vkCmdDispatch(cmd, gx, gy, 1);
}
void UIRenderer::CreatePipeline(const std::filesystem::path& spvPath) {
VulkanShader shader(spvPath, "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr);
// Spec: "If VkPipelineCreateFlags2CreateInfoKHR::flags includes
// VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT, layout must be
// VK_NULL_HANDLE." Push constants are then attached via
// vkCmdPushDataEXT at draw time, not via the layout.
VkPipelineCreateFlags2CreateInfo flags2{
.sType = VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO,
.flags = VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT,
};
VkComputePipelineCreateInfo info{
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.pNext = &flags2,
.stage = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
.module = shader.shader,
.pName = "main",
},
.layout = VK_NULL_HANDLE,
};
Device::CheckVkResult(vkCreateComputePipelines(
Device::device, VK_NULL_HANDLE, 1, &info, nullptr, &pipeline_));
}
// ─── descriptor writes ───────────────────────────────────────────────────
void UIRenderer::WriteSwapchainDescriptors() {
auto& heap = *window_->descriptorHeap;
// One write per (frame, frame index) pairing — same swapchain view per
// frame index for each per-frame heap copy.
std::array<VkImageDescriptorInfoEXT, Window::numFrames * Window::numFrames> infos{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames * Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames * Window::numFrames> destinations{};
std::size_t k = 0;
for (std::uint32_t heapFrame = 0; heapFrame < Window::numFrames; ++heapFrame) {
for (std::uint32_t imgFrame = 0; imgFrame < Window::numFrames; ++imgFrame) {
infos[k] = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
.pView = &window_->imageViews[imgFrame],
.layout = VK_IMAGE_LAYOUT_GENERAL,
};
resources[k] = {
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.data = { .pImage = &infos[k] },
};
destinations[k] = {
.address = heap.resourceHeap[heapFrame].value
+ heap.ImageByteOffset(static_cast<std::uint16_t>(outImageBase_ + imgFrame)),
.size = Device::descriptorHeapProperties.imageDescriptorSize,
};
++k;
}
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, static_cast<std::uint32_t>(k),
resources.data(), destinations.data()
);
}
void UIRenderer::WriteAtlasDescriptor() {
auto& heap = *window_->descriptorHeap;
// Build a stable VkImageViewCreateInfo for the atlas. ImageVulkan
// pre-creates a VkImageView, but the descriptor-heap path needs a
// pointer to a create-info — keep one on the renderer so the
// pointers we hand to vkWriteResourceDescriptorsEXT stay valid.
atlasViewCreateInfo_ = {
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = atlas.image.image,
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = VK_FORMAT_R8_UNORM,
.components = {
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
},
.subresourceRange = {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
std::array<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
infos[f] = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
.pView = &atlasViewCreateInfo_,
.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
};
resources[f] = {
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
.data = { .pImage = &infos[f] },
};
destinations[f] = {
.address = heap.resourceHeap[f].value
+ heap.ImageByteOffset(atlasImageSlot_),
.size = Device::descriptorHeapProperties.imageDescriptorSize,
};
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
}
void UIRenderer::WriteSamplerDescriptors() {
auto& heap = *window_->descriptorHeap;
VkSamplerCreateInfo info{
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.magFilter = VK_FILTER_LINEAR,
.minFilter = VK_FILTER_LINEAR,
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.maxAnisotropy = 1.0f,
.minLod = 0.0f,
.maxLod = VK_LOD_CLAMP_NONE,
};
std::array<VkSamplerCreateInfo, Window::numFrames> infos{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
infos[f] = info;
destinations[f] = {
.address = heap.samplerHeap[f].value
+ heap.SamplerByteOffset(linearSamplerSlot_),
.size = Device::descriptorHeapProperties.samplerDescriptorSize,
};
}
Device::vkWriteSamplerDescriptorsEXT(
Device::device, Window::numFrames, infos.data(), destinations.data()
);
}
void UIRenderer::WriteItemBufferDescriptors() {
auto& heap = *window_->descriptorHeap;
std::array<VkDeviceAddressRangeEXT, Window::numFrames * Window::numFrames> ranges{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames * Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames * Window::numFrames> destinations{};
std::size_t k = 0;
for (std::uint32_t heapFrame = 0; heapFrame < Window::numFrames; ++heapFrame) {
for (std::uint32_t bufFrame = 0; bufFrame < Window::numFrames; ++bufFrame) {
ranges[k] = {
.address = itemBufs_[bufFrame].address,
.size = itemBufs_[bufFrame].size,
};
resources[k] = {
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.data = { .pAddressRange = &ranges[k] },
};
destinations[k] = {
.address = heap.resourceHeap[heapFrame].value + heap.BufferByteOffset(static_cast<std::uint16_t>(itemBufBase_ + bufFrame)),
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
};
++k;
}
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, static_cast<std::uint32_t>(k),
resources.data(), destinations.data()
);
}
void UIRenderer::CreateLinearSampler() {
// Not used — VK_EXT_descriptor_heap writes the sampler create-info
// directly into the heap (see WriteSamplerDescriptors).
}

View file

@ -1,171 +0,0 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#include "vulkan/vulkan.h"
module Crafter.Graphics:UIScene_impl;
import :UIScene;
import :Window;
import :Types;
import :DescriptorHeapVulkan;
import :UIRenderer;
import :UIHit;
import :UILayout;
import :UIDrawList;
import :UIWidget;
import Crafter.Event;
import std;
using namespace Crafter;
using namespace Crafter::UI;
UIScene::~UIScene() {
// Release listeners before the rest of the scene tears down.
mouseListener_.reset();
updateListener_.reset();
textListener_.reset();
keyListener_.reset();
focused_ = nullptr;
if (window_) {
// De-register the renderer pass.
auto& v = window_->passes;
v.erase(std::remove(v.begin(), v.end(), static_cast<RenderPass*>(&renderer)), v.end());
// Clear the descriptor-heap pointer if we owned it; the heap's
// destructor releases its Vulkan buffers on its own.
if (ownsHeap_ && window_->descriptorHeap == &ownedHeap_) {
window_->descriptorHeap = nullptr;
}
}
}
float UIScene::WindowScale() const {
if (!window_) return 1.0f;
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
return window_->scale;
#else
return 1.0f;
#endif
}
void UIScene::Initialize(Window& window, const std::filesystem::path& spvPath) {
window_ = &window;
// Auto-create a heap for UI-only apps. Generous defaults so most
// user-augmented heaps will fit too — if the user wants to share with
// 3D content, they should pre-create their own heap and attach it
// before calling Initialize.
if (!window.descriptorHeap) {
ownedHeap_.Initialize(/*images*/ 388, /*buffers*/ 35, /*samplers*/ 17);
window.descriptorHeap = &ownedHeap_;
ownsHeap_ = true;
}
// One-shot init — needed by the atlas image transition. Each
// StartInit/FinishInit pair reuses the per-frame command buffer.
VkCommandBuffer cmd = window.StartInit();
renderer.Initialize(window, cmd, spvPath);
window.FinishInit();
// Register as a RenderPass (after any other pass already in
// window.passes — typically RTPass for mixed scenes).
window.passes.push_back(&renderer);
// Mouse: update focus to the topmost focusable under the cursor (or
// null if none), then dispatch the click via the bubble chain.
mouseListener_ = std::make_unique<EventListener<void>>(
&window.onMouseLeftClick,
[this]() {
if (!root_) return;
float x = window_->currentMousePos.x;
float y = window_->currentMousePos.y;
Widget* hit = UI::HitTest(*root_, x, y);
Widget* focusTarget = nullptr;
for (Widget* w = hit; w != nullptr; w = w->parent) {
if (w->IsFocusable()) { focusTarget = w; break; }
}
SetFocus(focusTarget);
UI::DispatchClick(*root_, x, y);
}
);
// Text input: only the currently-focused widget receives it.
textListener_ = std::make_unique<EventListener<const std::string_view>>(
&window.onTextInput,
[this](std::string_view t) {
if (focused_) focused_->OnTextInput(t);
}
);
// Non-character keys (Backspace, arrows, Enter, …).
keyListener_ = std::make_unique<EventListener<CrafterKeys>>(
&window.onAnyKeyDown,
[this](CrafterKeys key) {
if (focused_) focused_->OnKeyDown(key);
}
);
// Per-frame: re-layout, emit, push items. We capture FrameTime here
// so we can advance the scene's clock (caret blink, animations).
updateListener_ = std::make_unique<EventListener<FrameTime>>(
&window.onUpdate,
[this](FrameTime ft) {
elapsedSec_ += static_cast<float>(ft.delta.count());
RebuildFrame();
}
);
}
void UIScene::SetFocus(Widget* w) {
if (w == focused_) return;
if (focused_) focused_->OnBlur();
focused_ = w;
if (focused_) focused_->OnFocus();
}
void UIScene::RebuildFrame() {
if (!root_ || !window_) return;
float sc = WindowScale();
// Layout the tree against the current surface size.
UI::RunLayout(
*root_,
{ static_cast<float>(window_->width), static_cast<float>(window_->height) },
sc
);
// Emit draw items.
drawList.Reset();
drawList.atlas = &renderer.atlas;
drawList.bindlessBaseHeapIdx = renderer.BindlessBaseHeapIdx();
drawList.scale = sc;
drawList.time = elapsedSec_;
if (background_) {
drawList.AddRect(
{ 0, 0, static_cast<float>(window_->width), static_cast<float>(window_->height) },
*background_
);
}
UI::EmitTree(*root_, drawList);
// Stage to GPU.
renderer.SetItems(drawList.items);
}

View file

@ -516,109 +516,113 @@ void Window::SetTitle(const std::string_view title) {
#endif
}
void Window::SetCusorImage(std::uint16_t sizeX, std::uint16_t sizeY) {
void Window::SetCursorImage(std::uint16_t width, std::uint16_t height,
std::uint16_t hotspotX, std::uint16_t hotspotY,
const std::uint8_t* pixels) {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
if(cursorSurface == nullptr) {
if (width == 0 || height == 0 || pixels == nullptr) {
SetDefaultCursor();
return;
}
if (cursorSurface == nullptr) {
cursorSurface = wl_compositor_create_surface(Device::compositor);
}
int stride = width * 4;
int size = stride * height;
// Reuse the existing mmap+buffer if the size is unchanged; otherwise
// tear down and re-allocate.
if (cursorWlBuffer != nullptr &&
cursorBufferOldSize == static_cast<std::uint32_t>(size)) {
// size unchanged — keep the buffer and mmap.
} else {
if (cursorMmap_) {
munmap(cursorMmap_, cursorBufferOldSize);
cursorMmap_ = nullptr;
}
if (cursorWlBuffer) {
wl_buffer_destroy(cursorWlBuffer);
cursorWlBuffer = nullptr;
}
int fd = create_shm_file(size);
if (fd < 0) {
throw std::runtime_error(std::format(
"Window::SetCursorImage: shm allocation for {}B failed", size));
}
void* mapped = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
close(fd);
throw std::runtime_error("Window::SetCursorImage: mmap failed");
}
cursorMmap_ = static_cast<std::uint8_t*>(mapped);
wl_shm_pool* pool = wl_shm_create_pool(Device::shm, fd, size);
cursorWlBuffer = wl_shm_pool_create_buffer(
pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888);
wl_shm_pool_destroy(pool);
close(fd);
cursorBufferOldSize = static_cast<std::uint32_t>(size);
}
// Convert the user's straight-alpha RGBA8 pixels into the compositor's
// expected premultiplied BGRA8 (= ARGB8888 little-endian byte order).
for (int i = 0; i < width * height; ++i) {
std::uint8_t r = pixels[i * 4 + 0];
std::uint8_t g = pixels[i * 4 + 1];
std::uint8_t b = pixels[i * 4 + 2];
std::uint8_t a = pixels[i * 4 + 3];
cursorMmap_[i * 4 + 0] = static_cast<std::uint8_t>((b * a) / 255);
cursorMmap_[i * 4 + 1] = static_cast<std::uint8_t>((g * a) / 255);
cursorMmap_[i * 4 + 2] = static_cast<std::uint8_t>((r * a) / 255);
cursorMmap_[i * 4 + 3] = a;
}
cursorHotspotX_ = hotspotX;
cursorHotspotY_ = hotspotY;
wl_surface_attach(cursorSurface, cursorWlBuffer, 0, 0);
wl_surface_damage(cursorSurface, 0, 0, width, height);
wl_surface_commit(cursorSurface);
// If the pointer is currently inside our window, re-apply the cursor
// so the new hotspot takes effect immediately. Otherwise the next
// pointer-enter event will pick it up.
if (Device::wlPointer && Device::focusedWindow == this && lastPointerSerial_) {
wl_pointer_set_cursor(Device::wlPointer, lastPointerSerial_,
cursorSurface, hotspotX, hotspotY);
}
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
// Win32 cursor support is not implemented for the v2 Window.
(void)width; (void)height; (void)hotspotX; (void)hotspotY; (void)pixels;
#endif
}
void Window::SetDefaultCursor() {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
if (cursorMmap_) {
munmap(cursorMmap_, cursorBufferOldSize);
cursorMmap_ = nullptr;
}
if (cursorWlBuffer) {
wl_buffer_destroy(cursorWlBuffer);
cursorWlBuffer = nullptr;
}
int stride = sizeX * 4;
int size = stride * sizeY;
cursorBufferOldSize = size;
// Allocate a shared memory file with the right size
int fd = create_shm_file(size);
if (fd < 0) {
throw std::runtime_error(std::format("creating a buffer file for {}B failed", size));
}
wl_shm_pool *pool = wl_shm_create_pool(Device::shm, fd, size);
cursorWlBuffer = wl_shm_pool_create_buffer(pool, 0, sizeX, sizeY, stride, WL_SHM_FORMAT_ARGB8888);
wl_shm_pool_destroy(pool);
close(fd);
wl_surface_attach(cursorSurface, cursorWlBuffer, 0, 0);
wl_surface_damage(cursorSurface, 0, 0, sizeX, sizeY);
wl_surface_commit(cursorSurface);
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
if (cursorBitmap) {
DeleteObject(cursorBitmap);
if (cursorSurface) {
wl_surface_destroy(cursorSurface);
cursorSurface = nullptr;
}
if (cursorHandle) {
DestroyCursor(cursorHandle);
cursorHandle = nullptr;
}
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = sizeX;
bmi.bmiHeader.biHeight = -(int)sizeY; // top-down
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
HDC hdc = GetDC(nullptr);
cursorBitmap = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, reinterpret_cast<void**>(&cursorRenderer.buffer[0]), nullptr, 0);
ReleaseDC(nullptr, hdc);
if (!cursorBitmap) {
throw std::runtime_error("CreateDIBSection failed for cursor");
}
cursorSizeX = sizeX;
cursorSizeY = sizeY;
#endif
}
void Window::SetCusorImageDefault() {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
wl_buffer_destroy(cursorWlBuffer);
wl_surface_destroy(cursorSurface);
cursorSurface = nullptr;
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
if (cursorHandle) {
DestroyCursor(cursorHandle);
cursorHandle = nullptr;
}
if (cursorBitmap) {
DeleteObject(cursorBitmap);
cursorBitmap = nullptr;
}
// Setting nullptr will make WM_SETCURSOR fall through to the default
#endif
}
void Window::UpdateCursorImage() {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
wl_surface_attach(cursorSurface, cursorWlBuffer, 0, 0);
wl_surface_damage(cursorSurface, 0, 0, 9999999, 99999999);
wl_surface_commit(cursorSurface);
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
// Create a mask bitmap (all zeros = fully opaque, alpha comes from color bitmap)
HBITMAP hMask = CreateBitmap(cursorSizeX, cursorSizeY, 1, 1, nullptr);
ICONINFO ii = {};
ii.fIcon = FALSE;
ii.xHotspot = 0;
ii.yHotspot = 0;
ii.hbmMask = hMask;
ii.hbmColor = cursorBitmap;
if (cursorHandle) {
DestroyCursor(cursorHandle);
}
cursorHandle = (HCURSOR)CreateIconIndirect(&ii);
DeleteObject(hMask);
if (cursorHandle) {
SetCursor(cursorHandle);
cursorBufferOldSize = 0;
cursorHotspotX_ = 0;
cursorHotspotY_ = 0;
// Tell the compositor to drop our cursor surface — passing nullptr
// makes it fall back to the system default.
if (Device::wlPointer && Device::focusedWindow == this && lastPointerSerial_) {
wl_pointer_set_cursor(Device::wlPointer, lastPointerSerial_, nullptr, 0, 0);
}
#endif
}