UI rewrite 3rd attempt
This commit is contained in:
parent
c9fd1b1585
commit
1f5697326c
48 changed files with 2155 additions and 6190 deletions
56
interfaces/Crafter.Graphics-ComputeShader.cppm
Normal file
56
interfaces/Crafter.Graphics-ComputeShader.cppm
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
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"
|
||||
export module Crafter.Graphics:ComputeShader;
|
||||
import std;
|
||||
import :Device;
|
||||
|
||||
export namespace Crafter {
|
||||
// Tier 1: thin compute-pipeline wrapper. Owns one VkPipeline created with
|
||||
// VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT (no pipeline layout — the
|
||||
// bindless heap supplies all bindings, push constants travel via
|
||||
// vkCmdPushDataEXT). Use this to dispatch the four standard UI shaders
|
||||
// and any user-authored compute shader that follows the ui-shared.glsl
|
||||
// contract.
|
||||
class ComputeShader {
|
||||
public:
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
|
||||
ComputeShader() = default;
|
||||
ComputeShader(const ComputeShader&) = delete;
|
||||
ComputeShader& operator=(const ComputeShader&) = delete;
|
||||
ComputeShader(ComputeShader&& other) noexcept;
|
||||
ComputeShader& operator=(ComputeShader&& other) noexcept;
|
||||
~ComputeShader();
|
||||
|
||||
// Loads a SPIR-V compute shader from disk and creates a pipeline that
|
||||
// uses the bindless descriptor-heap binding model.
|
||||
void Load(const std::filesystem::path& spvPath);
|
||||
|
||||
// Bind, push constants (if any), dispatch. Caller computes group counts
|
||||
// and is responsible for any inter-dispatch barriers (UIRenderer::Dispatch
|
||||
// wraps this with the standard write-after-write barrier).
|
||||
void Dispatch(VkCommandBuffer cmd,
|
||||
const void* push, std::uint32_t pushBytes,
|
||||
std::uint32_t gx,
|
||||
std::uint32_t gy = 1,
|
||||
std::uint32_t gz = 1) const;
|
||||
};
|
||||
}
|
||||
|
|
@ -18,13 +18,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
*/
|
||||
module;
|
||||
#include "vulkan/vulkan.h"
|
||||
export module Crafter.Graphics:UIAtlas;
|
||||
export module Crafter.Graphics:FontAtlas;
|
||||
import std;
|
||||
import :Font;
|
||||
import :ImageVulkan;
|
||||
import :Device;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
export namespace Crafter {
|
||||
// Per-glyph metrics. UVs are 0..1 in atlas space; on-screen sizes /
|
||||
// offsets / advance are in *atlas pixels at the base size* and scale
|
||||
// linearly with the requested font size at draw time.
|
||||
|
|
@ -16,15 +16,308 @@ 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"
|
||||
export module Crafter.Graphics:UI;
|
||||
import std;
|
||||
import Crafter.Event;
|
||||
import :Device;
|
||||
import :Window;
|
||||
import :RenderPass;
|
||||
import :DescriptorHeapVulkan;
|
||||
import :ImageVulkan;
|
||||
import :VulkanBuffer;
|
||||
import :ComputeShader;
|
||||
import :FontAtlas;
|
||||
import :Font;
|
||||
|
||||
export import :UILength;
|
||||
export import :UIWidget;
|
||||
export import :UILayout;
|
||||
export import :UIDrawList;
|
||||
export import :UIAtlas;
|
||||
export import :UIWidgets;
|
||||
export import :UITheme;
|
||||
export import :UIHit;
|
||||
export import :UIRenderer;
|
||||
export import :UIScene;
|
||||
export namespace Crafter {
|
||||
// ─── push-constant header ───────────────────────────────────────────
|
||||
// Mirrors shaders/ui-shared.glsl::UIDispatchHeader byte-for-byte. User
|
||||
// shaders MUST embed this as the first member of their push-constant
|
||||
// struct so UIRenderer::FillHeader works.
|
||||
struct UIDispatchHeader {
|
||||
std::uint32_t outImage;
|
||||
std::uint32_t itemBuffer;
|
||||
std::uint32_t surfaceWidth;
|
||||
std::uint32_t surfaceHeight;
|
||||
float clipX, clipY, clipW, clipH;
|
||||
std::uint32_t itemCount;
|
||||
std::uint32_t frameIdx;
|
||||
std::uint32_t flags;
|
||||
std::uint32_t _pad;
|
||||
};
|
||||
static_assert(sizeof(UIDispatchHeader) == 48);
|
||||
|
||||
// ─── standard item PODs (match GLSL std430) ─────────────────────────
|
||||
struct QuadItem {
|
||||
float x, y, w, h;
|
||||
float r, g, b, a;
|
||||
float cTL, cTR, cBR, cBL; // per-corner radius in px
|
||||
float outline, oR, oG, oB; // outline thickness + RGB
|
||||
};
|
||||
static_assert(sizeof(QuadItem) == 64);
|
||||
|
||||
struct CircleItem {
|
||||
float cx, cy, radius, _p0;
|
||||
float r, g, b, a;
|
||||
float outline, oR, oG, oB;
|
||||
};
|
||||
static_assert(sizeof(CircleItem) == 48);
|
||||
|
||||
struct ImageItem {
|
||||
float x, y, w, h;
|
||||
float u0, v0, u1, v1;
|
||||
float tR, tG, tB, tA;
|
||||
std::uint32_t texSlot, sampSlot, _p1, _p2;
|
||||
};
|
||||
static_assert(sizeof(ImageItem) == 64);
|
||||
|
||||
struct GlyphItem {
|
||||
float x, y, w, h;
|
||||
float u0, v0, u1, v1;
|
||||
float r, g, b, a;
|
||||
};
|
||||
static_assert(sizeof(GlyphItem) == 48);
|
||||
|
||||
// ─── tiny rect-carving helper ───────────────────────────────────────
|
||||
// Pure value semantics. No engine, just convenience. Skip if you'd rather
|
||||
// compute pixels yourself.
|
||||
struct Rect {
|
||||
float x = 0, y = 0, w = 0, h = 0;
|
||||
|
||||
enum class Anchor { Top, Bottom, Left, Right };
|
||||
|
||||
// Returns a sub-rect of `size` along the given anchor edge of self.
|
||||
// Does not modify `*this`. (Use `.Inset(...)` to drop a margin first.)
|
||||
Rect SubRect(float size, Anchor a) const noexcept {
|
||||
switch (a) {
|
||||
case Anchor::Top: return { x, y, w, std::min(size, h) };
|
||||
case Anchor::Bottom: return { x, y + h - std::min(size, h), w, std::min(size, h) };
|
||||
case Anchor::Left: return { x, y, std::min(size, w), h };
|
||||
case Anchor::Right: return { x + w - std::min(size, w), y, std::min(size, w), h };
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Rect Inset(float padding) const noexcept {
|
||||
return Inset(padding, padding, padding, padding);
|
||||
}
|
||||
Rect Inset(float top, float right, float bottom, float left) const noexcept {
|
||||
float nw = std::max(0.0f, w - left - right);
|
||||
float nh = std::max(0.0f, h - top - bottom);
|
||||
return { x + left, y + top, nw, nh };
|
||||
}
|
||||
|
||||
bool Contains(float px, float py) const noexcept {
|
||||
return px >= x && px < x + w && py >= y && py < y + h;
|
||||
}
|
||||
|
||||
static Rect FromWindow(const Window& win) noexcept {
|
||||
return { 0, 0, static_cast<float>(win.width), static_cast<float>(win.height) };
|
||||
}
|
||||
};
|
||||
|
||||
// ─── per-frame callback args ────────────────────────────────────────
|
||||
struct UIBuildArgs {
|
||||
VkCommandBuffer cmd;
|
||||
std::uint32_t frameIdx;
|
||||
};
|
||||
|
||||
// ─── UIRenderer ─────────────────────────────────────────────────────
|
||||
// One per Window (typically). Owns the four standard compute shaders,
|
||||
// pre-allocates heap slots for the swapchain images, and exposes a thin
|
||||
// dispatch helper for both the standard shaders and user-supplied ones.
|
||||
//
|
||||
// Workflow:
|
||||
// 1. Construct, configure (set fontAtlas if drawing text).
|
||||
// 2. Initialize(window, heap, initCmd) — once, after window.descriptorHeap
|
||||
// is set and before window.FinishInit().
|
||||
// 3. window.passes.push_back(&ui).
|
||||
// 4. Listen on `onBuild`. Inside the callback, fill your item buffers,
|
||||
// flush them, and call DispatchQuads / DispatchCircles / DispatchImages
|
||||
// / DispatchText / Dispatch as needed. Library inserts a SHADER_WRITE
|
||||
// → SHADER_READ|WRITE memory barrier between consecutive dispatches.
|
||||
class UIRenderer : public RenderPass {
|
||||
public:
|
||||
// Pre-loaded standard shaders (public so users can call Dispatch
|
||||
// directly with them if they want to embed extra push-constant fields
|
||||
// beyond the standard header).
|
||||
ComputeShader drawQuads;
|
||||
ComputeShader drawCircles;
|
||||
ComputeShader drawImages;
|
||||
ComputeShader drawText;
|
||||
|
||||
// Optional. If set before Initialize, the atlas is registered into a
|
||||
// sampled-image slot + linear sampler slot, and Update(cmd) is called
|
||||
// at the top of every Record() so any glyphs ensured during onBuild
|
||||
// make it to the GPU before the text dispatch reads them.
|
||||
FontAtlas* fontAtlas = nullptr;
|
||||
|
||||
// User callback. Subscribe by holding a Crafter::EventListener<UIBuildArgs>:
|
||||
// EventListener<UIBuildArgs> sub(&ui.onBuild, [&](UIBuildArgs a) { ... });
|
||||
// Listener lifetime governs the subscription.
|
||||
Crafter::Event<UIBuildArgs> onBuild;
|
||||
|
||||
UIRenderer() = default;
|
||||
UIRenderer(const UIRenderer&) = delete;
|
||||
UIRenderer& operator=(const UIRenderer&) = delete;
|
||||
|
||||
// Default shader paths assume Crafter.Build placed the .spv files
|
||||
// alongside the consumer binary (this is what cfg.shaders does).
|
||||
void Initialize(Window& window, DescriptorHeapVulkan& heap, VkCommandBuffer initCmd,
|
||||
std::filesystem::path quadsSpv = "ui-quads.comp.spv",
|
||||
std::filesystem::path circlesSpv = "ui-circles.comp.spv",
|
||||
std::filesystem::path imagesSpv = "ui-images.comp.spv",
|
||||
std::filesystem::path textSpv = "ui-text.comp.spv");
|
||||
|
||||
// RenderPass interface — invoked from Window::Render.
|
||||
void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override;
|
||||
|
||||
// ─── helpers used inside `onBuild` ─────────────────────────────
|
||||
|
||||
// Builds a populated header. `clipRectPx` defaults to "no clip".
|
||||
UIDispatchHeader FillHeader(std::uint32_t itemBufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f},
|
||||
std::uint32_t flags = 0) const noexcept;
|
||||
|
||||
// Convenience: dispatches the named standard shader. Group count is
|
||||
// computed from the window's surface size — the standard shaders
|
||||
// dispatch one workgroup per 8×8 screen tile and iterate every item
|
||||
// in the buffer in order, so item ORDER in the buffer == draw order
|
||||
// on screen (later items overdraw earlier ones, race-free).
|
||||
void DispatchQuads(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
|
||||
void DispatchCircles(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
|
||||
void DispatchImages(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
|
||||
// For DispatchText, the font atlas image+sampler slots are taken from
|
||||
// UIRenderer's Initialize-time registration (see fontAtlasImageSlot()
|
||||
// / fontAtlasSamplerSlot()). Set `fontAtlas` before Initialize.
|
||||
void DispatchText(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
|
||||
|
||||
// Generic dispatch — for user-authored shaders. Inserts the standard
|
||||
// pre-dispatch barrier (skipped on the first call per frame).
|
||||
void Dispatch(VkCommandBuffer cmd, const ComputeShader& shader,
|
||||
const void* push, std::uint32_t pushBytes,
|
||||
std::uint32_t gx, std::uint32_t gy = 1, std::uint32_t gz = 1);
|
||||
|
||||
// Allocates a heap slot for the buffer and writes its descriptor into
|
||||
// every per-frame heap. The user's mapped buffer is shared across
|
||||
// frames — fine because Window::Render currently waits idle before
|
||||
// submitting the next frame. Returns the slot index for use in headers.
|
||||
template<typename T, bool Mapped>
|
||||
std::uint16_t RegisterBuffer(VulkanBuffer<T, Mapped>& buffer);
|
||||
|
||||
// Same for an ImageVulkan-managed sampled image (e.g. a user texture).
|
||||
// Caller specifies the layout the image will be sampled in (typically
|
||||
// VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL).
|
||||
template<typename Pixel>
|
||||
std::uint16_t RegisterImage(ImageVulkan<Pixel>& image, VkFormat format,
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
|
||||
// Allocates a sampler slot and writes a VkSamplerCreateInfo into
|
||||
// every per-frame sampler heap. v1 takes the create-info inline.
|
||||
std::uint16_t RegisterSampler(const VkSamplerCreateInfo& info);
|
||||
|
||||
// Convenience: a linear-filter, clamp-to-edge sampler. Returns the
|
||||
// slot. Useful for the FontAtlas and most plain image sampling.
|
||||
std::uint16_t RegisterLinearClampSampler();
|
||||
|
||||
// Shapes a UTF-8 string into glyph quads at (x, y) baseline. Calls
|
||||
// FontAtlas::Ensure for each codepoint (rasterising on first use),
|
||||
// emits one GlyphItem per visible glyph, returns the count written.
|
||||
// Use this to fill a GlyphItem buffer that you then dispatch.
|
||||
// Cursor advances along +X. No line-wrap, no kerning — single line.
|
||||
std::uint32_t 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 = nullptr);
|
||||
|
||||
// Read after Initialize: the slot the font atlas was registered into.
|
||||
// 0xFFFF means "no atlas" (set fontAtlas before Initialize).
|
||||
std::uint16_t FontAtlasImageSlot() const noexcept { return fontAtlasImageSlot_; }
|
||||
std::uint16_t FontAtlasSamplerSlot() const noexcept { return fontAtlasSamplerSlot_; }
|
||||
|
||||
private:
|
||||
Window* window_ = nullptr;
|
||||
DescriptorHeapVulkan* heap_ = nullptr;
|
||||
|
||||
// One image slot used for the swapchain output. In each per-frame
|
||||
// heap, that slot points at THAT frame's swapchain image. So the
|
||||
// shader's `uiImages[hdr.outImage]` is always the current frame's
|
||||
// swapchain image regardless of which heap is bound.
|
||||
std::uint16_t outImageSlot_ = 0;
|
||||
|
||||
// Stable VkImageViewCreateInfos for the descriptor heap to ingest.
|
||||
// These must outlive the write call.
|
||||
VkImageViewCreateInfo atlasViewCreateInfo_{};
|
||||
|
||||
std::uint16_t fontAtlasImageSlot_ = 0xFFFF;
|
||||
std::uint16_t fontAtlasSamplerSlot_ = 0xFFFF;
|
||||
|
||||
bool firstDispatchThisFrame_ = true;
|
||||
|
||||
void WriteSwapchainDescriptors();
|
||||
void WriteFontAtlasDescriptor();
|
||||
|
||||
// Helper used by RegisterBuffer template (defined in impl). Writes the
|
||||
// address-range descriptor at `slot` into all per-frame heaps.
|
||||
void WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size);
|
||||
|
||||
// Helper used by RegisterImage template — writes a sampled-image at
|
||||
// `slot` referring to a stable VkImageViewCreateInfo (caller stores).
|
||||
void WriteSampledImageDescriptor(std::uint16_t slot,
|
||||
const VkImageViewCreateInfo& viewInfo,
|
||||
VkImageLayout layout);
|
||||
};
|
||||
|
||||
// ─── template-method implementations ────────────────────────────────
|
||||
template<typename T, bool Mapped>
|
||||
std::uint16_t UIRenderer::RegisterBuffer(VulkanBuffer<T, Mapped>& buffer) {
|
||||
auto range = heap_->AllocateBufferSlots(1);
|
||||
WriteBufferDescriptor(range.firstElement, buffer.address, buffer.size);
|
||||
// GLSL `descriptor_heap` indexes buffer-typed views in buffer-descriptor
|
||||
// units from heap byte 0; the actual buffer region starts past the
|
||||
// image region at `bufferStartElement`. Return the absolute index so
|
||||
// the user just hands it to FillHeader without thinking about it.
|
||||
return static_cast<std::uint16_t>(heap_->bufferStartElement + range.firstElement);
|
||||
}
|
||||
|
||||
template<typename Pixel>
|
||||
std::uint16_t UIRenderer::RegisterImage(ImageVulkan<Pixel>& image, VkFormat format,
|
||||
VkImageLayout layout) {
|
||||
auto range = heap_->AllocateImageSlots(1);
|
||||
|
||||
// Build a stable view-create-info that lives as long as the heap reads
|
||||
// it. We co-locate it on the renderer for the font atlas; for arbitrary
|
||||
// user images we lean on the fact that vkWriteResourceDescriptorsEXT
|
||||
// copies the view descriptor immediately. (Validated by the heap spec:
|
||||
// the descriptor is materialised at write time, the create-info need
|
||||
// not persist past the call.)
|
||||
VkImageViewCreateInfo info {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.image = image.image,
|
||||
.viewType = VK_IMAGE_VIEW_TYPE_2D,
|
||||
.format = format,
|
||||
.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 = image.mipLevels,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1,
|
||||
},
|
||||
};
|
||||
WriteSampledImageDescriptor(range.firstElement, info, layout);
|
||||
return range.firstElement;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
115
interfaces/Crafter.Graphics-UIComponents.cppm
Normal file
115
interfaces/Crafter.Graphics-UIComponents.cppm
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
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"
|
||||
export module Crafter.Graphics:UIComponents;
|
||||
import std;
|
||||
import :UI;
|
||||
import :Font;
|
||||
import :FontAtlas;
|
||||
|
||||
// Tier 3: stateless presentation functions. These append items to the user's
|
||||
// QuadItem / GlyphItem buffers — they do NOT dispatch. The user dispatches
|
||||
// via UIRenderer::DispatchQuads / DispatchText after their onBuild fills
|
||||
// everything, so a frame stays one quads dispatch + one text dispatch
|
||||
// regardless of how many components were drawn.
|
||||
//
|
||||
// State for components that need it (hovered, pressed, dragging, t01) is the
|
||||
// USER's responsibility — these functions are pure presentation.
|
||||
//
|
||||
// EXTENSION MODEL: each function below is short on purpose. If you want a
|
||||
// hexagon button, an icon-with-label button, a tristate checkbox — copy the
|
||||
// function body into your code and modify it. There is no override hook.
|
||||
|
||||
export namespace Crafter {
|
||||
// Aggregate for the two item buffers + the optional text-shaping deps.
|
||||
// Build one per frame in onBuild and pass it to component calls.
|
||||
struct UIBuffer {
|
||||
QuadItem* quads = nullptr;
|
||||
std::uint32_t* quadCount = nullptr;
|
||||
std::uint32_t quadCap = 0;
|
||||
|
||||
GlyphItem* glyphs = nullptr;
|
||||
std::uint32_t* glyphCount = nullptr;
|
||||
std::uint32_t glyphCap = 0;
|
||||
|
||||
FontAtlas* atlas = nullptr; // for text-emitting components
|
||||
UIRenderer* renderer = nullptr; // for ShapeText
|
||||
};
|
||||
|
||||
// ─── per-component color blocks ─────────────────────────────────────
|
||||
// Inline POD aggregates. Users compose their own application-level theme
|
||||
// by holding a few of these together; the library has no Theme type.
|
||||
|
||||
struct ButtonColors {
|
||||
std::array<float, 4> bg;
|
||||
std::array<float, 4> bgHover;
|
||||
std::array<float, 4> bgPressed;
|
||||
std::array<float, 4> text;
|
||||
std::array<float, 4> border = {0, 0, 0, 0};
|
||||
float cornerRadius = 0;
|
||||
float borderThickness = 0;
|
||||
};
|
||||
|
||||
struct CheckboxColors {
|
||||
std::array<float, 4> bg;
|
||||
std::array<float, 4> bgHover;
|
||||
std::array<float, 4> check;
|
||||
std::array<float, 4> border = {0, 0, 0, 0};
|
||||
float cornerRadius = 4;
|
||||
float borderThickness = 1;
|
||||
float checkInset = 4; // px on each side
|
||||
};
|
||||
|
||||
struct SliderColors {
|
||||
std::array<float, 4> track;
|
||||
std::array<float, 4> trackFilled;
|
||||
std::array<float, 4> thumb;
|
||||
std::array<float, 4> thumbHover;
|
||||
float trackHeight = 4;
|
||||
float thumbRadius = 8;
|
||||
};
|
||||
|
||||
struct ProgressColors {
|
||||
std::array<float, 4> bg;
|
||||
std::array<float, 4> fill;
|
||||
float cornerRadius = 0;
|
||||
};
|
||||
|
||||
// ─── component functions ───────────────────────────────────────────
|
||||
|
||||
// Background quad (color depends on state) + centered label glyphs.
|
||||
void DrawButton(UIBuffer& buf, Rect r, std::string_view label,
|
||||
bool hovered, bool pressed,
|
||||
Font& font, float fontSize,
|
||||
const ButtonColors& c);
|
||||
|
||||
// Outlined quad + a smaller filled inset quad when `checked`.
|
||||
void DrawCheckbox(UIBuffer& buf, Rect r, bool checked, bool hovered,
|
||||
const CheckboxColors& c);
|
||||
|
||||
// Thin track quad split at `t01` into filled/empty + a circular thumb
|
||||
// (drawn as a quad with cornerRadius = thumbRadius).
|
||||
void DrawSlider(UIBuffer& buf, Rect r, float t01, bool dragging,
|
||||
const SliderColors& c);
|
||||
|
||||
// Background quad + a filled quad clipped to t01 of the inner width.
|
||||
void DrawProgressBar(UIBuffer& buf, Rect r, float t01,
|
||||
const ProgressColors& c);
|
||||
}
|
||||
|
|
@ -1,168 +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
|
||||
*/
|
||||
export module Crafter.Graphics:UIDrawList;
|
||||
import std;
|
||||
import :UILength;
|
||||
import :UIWidget;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
class FontAtlas; // forward decl (full def in :UIAtlas)
|
||||
|
||||
// Item type tags. Must match the shader-side constants exactly.
|
||||
enum class ItemType : std::uint32_t {
|
||||
Rect = 0,
|
||||
RoundRect = 1,
|
||||
Glyph = 2,
|
||||
Image = 3,
|
||||
ClipPush = 5,
|
||||
ClipPop = 6,
|
||||
};
|
||||
|
||||
// GPU-bound draw item. Layout matches the shader's UIItem struct under
|
||||
// GL_EXT_scalar_block_layout (no std140/std430 padding). Keep this in
|
||||
// sync with shaders/ui.comp.glsl.
|
||||
//
|
||||
// Field meanings by ItemType:
|
||||
// Rect: posPx, sizePx, color (alpha-premultiplied).
|
||||
// RoundRect: same as Rect + cornerRadiusPx.
|
||||
// Glyph: posPx/sizePx = on-screen quad; uvRect = atlas region;
|
||||
// color tints the SDF sample; cornerRadiusPx unused.
|
||||
// Image: posPx/sizePx = quad; uvRect = source rect (0..1);
|
||||
// imageIdx = bindless slot offset; color tints.
|
||||
// ClipPush: posPx/sizePx = clip rect to push (intersected with current).
|
||||
// ClipPop: fields ignored.
|
||||
struct UIItem {
|
||||
std::uint32_t type; // ItemType
|
||||
std::uint32_t flags;
|
||||
float posPx[2];
|
||||
float sizePx[2];
|
||||
float color[4];
|
||||
float colorB[4];
|
||||
float uvRect[4];
|
||||
std::uint32_t imageIdx;
|
||||
std::uint32_t cornerRadiusPx;
|
||||
float reserved[2];
|
||||
};
|
||||
static_assert(sizeof(UIItem) == 88, "UIItem size must match shader-side struct");
|
||||
|
||||
// CPU-side accumulator. Widgets call `Add(...)` (or convenience helpers)
|
||||
// during their Emit pass; the renderer copies the resulting buffer into
|
||||
// the per-frame mapped SSBO and dispatches the compute shader.
|
||||
class DrawList {
|
||||
public:
|
||||
std::vector<UIItem> items;
|
||||
|
||||
// Set by the renderer before EmitTree(). Widgets that draw text or
|
||||
// images consult these — without an atlas, glyph emission is a
|
||||
// no-op (useful for layout-only debug dumps).
|
||||
FontAtlas* atlas = nullptr;
|
||||
std::uint32_t bindlessBaseHeapIdx = 0; // base heap slot for Image widgets
|
||||
float scale = 1.0f; // device scale (mirrors LayoutContext::scale)
|
||||
float time = 0.0f; // seconds since scene init (drives blink etc.)
|
||||
|
||||
void Reset() { items.clear(); }
|
||||
|
||||
void Add(const UIItem& it) { items.push_back(it); }
|
||||
|
||||
// Convenience constructors for common items. These keep widget
|
||||
// Emit code short and self-documenting.
|
||||
void AddRect(Rect r, Color c) {
|
||||
UIItem it{};
|
||||
it.type = static_cast<std::uint32_t>(ItemType::Rect);
|
||||
it.posPx[0] = r.x; it.posPx[1] = r.y;
|
||||
it.sizePx[0] = r.w; it.sizePx[1] = r.h;
|
||||
// Premultiply alpha so the shader's "OVER" operator works without
|
||||
// a per-pixel multiply.
|
||||
it.color[0] = c.r * c.a;
|
||||
it.color[1] = c.g * c.a;
|
||||
it.color[2] = c.b * c.a;
|
||||
it.color[3] = c.a;
|
||||
items.push_back(it);
|
||||
}
|
||||
|
||||
void AddRoundRect(Rect r, Color c, float radiusPx) {
|
||||
UIItem it{};
|
||||
it.type = static_cast<std::uint32_t>(ItemType::RoundRect);
|
||||
it.posPx[0] = r.x; it.posPx[1] = r.y;
|
||||
it.sizePx[0] = r.w; it.sizePx[1] = r.h;
|
||||
it.color[0] = c.r * c.a;
|
||||
it.color[1] = c.g * c.a;
|
||||
it.color[2] = c.b * c.a;
|
||||
it.color[3] = c.a;
|
||||
it.cornerRadiusPx = static_cast<std::uint32_t>(radiusPx);
|
||||
items.push_back(it);
|
||||
}
|
||||
|
||||
// Glyph item: `quad` is the glyph's on-screen rect, `atlasUV` is
|
||||
// its (x, y, w, h) region in 0..1 atlas-UV space.
|
||||
void AddGlyph(Rect quad, Color color, std::array<float, 4> atlasUV) {
|
||||
UIItem it{};
|
||||
it.type = static_cast<std::uint32_t>(ItemType::Glyph);
|
||||
it.posPx[0] = quad.x; it.posPx[1] = quad.y;
|
||||
it.sizePx[0] = quad.w; it.sizePx[1] = quad.h;
|
||||
it.color[0] = color.r * color.a;
|
||||
it.color[1] = color.g * color.a;
|
||||
it.color[2] = color.b * color.a;
|
||||
it.color[3] = color.a;
|
||||
it.uvRect[0] = atlasUV[0]; it.uvRect[1] = atlasUV[1];
|
||||
it.uvRect[2] = atlasUV[2]; it.uvRect[3] = atlasUV[3];
|
||||
items.push_back(it);
|
||||
}
|
||||
|
||||
// Image item: `imageHeapOffset` is added to the renderer's
|
||||
// bindless-base slot at draw time to find the right descriptor.
|
||||
void AddImage(Rect quad, Color tint, std::uint32_t imageHeapOffset,
|
||||
std::array<float, 4> sourceUV = {0, 0, 1, 1}) {
|
||||
UIItem it{};
|
||||
it.type = static_cast<std::uint32_t>(ItemType::Image);
|
||||
it.posPx[0] = quad.x; it.posPx[1] = quad.y;
|
||||
it.sizePx[0] = quad.w; it.sizePx[1] = quad.h;
|
||||
it.color[0] = tint.r * tint.a;
|
||||
it.color[1] = tint.g * tint.a;
|
||||
it.color[2] = tint.b * tint.a;
|
||||
it.color[3] = tint.a;
|
||||
it.uvRect[0] = sourceUV[0]; it.uvRect[1] = sourceUV[1];
|
||||
it.uvRect[2] = sourceUV[2]; it.uvRect[3] = sourceUV[3];
|
||||
it.imageIdx = imageHeapOffset;
|
||||
items.push_back(it);
|
||||
}
|
||||
|
||||
// Clip stack — emit a ClipPush at the start of the clipped region
|
||||
// and a matching ClipPop at the end. The shader maintains a small
|
||||
// fixed-size stack and intersects pushes with the existing clip.
|
||||
void PushClip(Rect r) {
|
||||
UIItem it{};
|
||||
it.type = static_cast<std::uint32_t>(ItemType::ClipPush);
|
||||
it.posPx[0] = r.x; it.posPx[1] = r.y;
|
||||
it.sizePx[0] = r.w; it.sizePx[1] = r.h;
|
||||
items.push_back(it);
|
||||
}
|
||||
|
||||
void PopClip() {
|
||||
UIItem it{};
|
||||
it.type = static_cast<std::uint32_t>(ItemType::ClipPop);
|
||||
items.push_back(it);
|
||||
}
|
||||
};
|
||||
|
||||
// Walk the laid-out tree and emit every widget's items.
|
||||
inline void EmitTree(const Widget& root, DrawList& dl) {
|
||||
root.Emit(dl);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +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
|
||||
*/
|
||||
export module Crafter.Graphics:UIHit;
|
||||
import std;
|
||||
import :UILength;
|
||||
import :UIWidget;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
// Find the topmost widget whose computedRect contains (x, y).
|
||||
// Children are visited in reverse order so later children (drawn on
|
||||
// top) win ties. Returns nullptr if the point is outside `root`.
|
||||
inline Widget* HitTest(Widget& root, float x, float y) {
|
||||
if (!root.computedRect.Contains(x, y)) return nullptr;
|
||||
|
||||
// Search children in reverse — the last-added child is on top in
|
||||
// our draw order, so it wins overlapping hits.
|
||||
for (auto it = root.children_.rbegin(); it != root.children_.rend(); ++it) {
|
||||
if (Widget* hit = HitTest(**it, x, y); hit) return hit;
|
||||
}
|
||||
return &root;
|
||||
}
|
||||
|
||||
// Dispatch a click at (x, y) to the topmost widget under the cursor,
|
||||
// bubbling to ancestors until one returns true (handled). The default
|
||||
// Widget::OnMouseClick returns false, so leaf widgets that don't care
|
||||
// automatically defer to their parents.
|
||||
inline void DispatchClick(Widget& root, float x, float y) {
|
||||
Widget* target = HitTest(root, x, y);
|
||||
while (target) {
|
||||
if (target->OnMouseClick(x, y)) return;
|
||||
target = target->parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +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
|
||||
*/
|
||||
export module Crafter.Graphics:UILayout;
|
||||
import std;
|
||||
import :UILength;
|
||||
import :UIWidget;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
// Convert a Length to device pixels. `parentExtent` is the parent's
|
||||
// available extent on the same axis (already in device px). `autoFn`
|
||||
// produces the size to use for `Auto` and `Frac` modes — for Auto this
|
||||
// is the desired-content size, for Frac it's the same fallback (Frac
|
||||
// is meaningful only inside a stack container, which resolves it
|
||||
// separately; everywhere else it's just "fill what's available", same
|
||||
// as Auto).
|
||||
template<typename AutoFn>
|
||||
constexpr float ResolveLength(Length len, float parentExtent, float scale, AutoFn&& autoFn) {
|
||||
switch (len.mode) {
|
||||
case Length::Mode::Px: return len.value * scale;
|
||||
case Length::Mode::Pct: return len.value * 0.01f * parentExtent;
|
||||
case Length::Mode::Auto: return static_cast<float>(autoFn());
|
||||
case Length::Mode::Frac: return static_cast<float>(autoFn());
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Edges resolved into device pixels (no Length involvement; Edges are
|
||||
// already plain floats in logical px).
|
||||
struct EdgesPx {
|
||||
float top = 0, right = 0, bottom = 0, left = 0;
|
||||
constexpr float Horiz() const { return left + right; }
|
||||
constexpr float Vert() const { return top + bottom; }
|
||||
};
|
||||
|
||||
constexpr EdgesPx ResolveEdges(Edges e, float scale) {
|
||||
return { e.top * scale, e.right * scale, e.bottom * scale, e.left * scale };
|
||||
}
|
||||
|
||||
// Rect minus padding — yields the content rect.
|
||||
constexpr Rect ShrinkBy(Rect r, EdgesPx p) {
|
||||
return {
|
||||
r.x + p.left,
|
||||
r.y + p.top,
|
||||
std::max(0.0f, r.w - p.Horiz()),
|
||||
std::max(0.0f, r.h - p.Vert()),
|
||||
};
|
||||
}
|
||||
|
||||
// Run the two-pass measure/arrange on a root widget bound to a surface
|
||||
// of `surfacePx` device pixels at `scale`. The root receives the full
|
||||
// surface as its arrange rect.
|
||||
inline void RunLayout(Widget& root, Size surfacePx, float scale) {
|
||||
LayoutContext ctx{ .scale = scale, .surfaceSize = surfacePx };
|
||||
root.Measure(surfacePx, ctx);
|
||||
root.Arrange({0, 0, surfacePx.w, surfacePx.h}, ctx);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +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
|
||||
*/
|
||||
export module Crafter.Graphics:UILength;
|
||||
import std;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
struct Length {
|
||||
enum class Mode : std::uint8_t { Px, Pct, Auto, Frac };
|
||||
Mode mode = Mode::Auto;
|
||||
float value = 0.0f;
|
||||
|
||||
static constexpr Length Px(float v) { return {Mode::Px, v}; }
|
||||
static constexpr Length Pct(float v) { return {Mode::Pct, v}; }
|
||||
static constexpr Length Auto() { return {Mode::Auto, 0.0f}; }
|
||||
static constexpr Length Frac(float v) { return {Mode::Frac, v}; }
|
||||
};
|
||||
|
||||
enum class Anchor : std::uint8_t {
|
||||
TopLeft, Top, TopRight,
|
||||
Left, Center, Right,
|
||||
BottomLeft, Bottom, BottomRight,
|
||||
};
|
||||
|
||||
struct Edges {
|
||||
float top = 0, right = 0, bottom = 0, left = 0;
|
||||
|
||||
constexpr Edges() = default;
|
||||
constexpr explicit Edges(float all) : top(all), right(all), bottom(all), left(all) {}
|
||||
constexpr Edges(float vert, float horiz) : top(vert), right(horiz), bottom(vert), left(horiz) {}
|
||||
constexpr Edges(float t, float r, float b, float l) : top(t), right(r), bottom(b), left(l) {}
|
||||
};
|
||||
|
||||
struct Color {
|
||||
float r = 0, g = 0, b = 0, a = 1;
|
||||
|
||||
constexpr Color() = default;
|
||||
constexpr Color(float r, float g, float b, float a = 1.0f) : r(r), g(g), b(b), a(a) {}
|
||||
|
||||
// 0xRRGGBB, alpha = 1.0
|
||||
static constexpr Color rgb(std::uint32_t hex) {
|
||||
return {
|
||||
((hex >> 16) & 0xFF) / 255.0f,
|
||||
((hex >> 8) & 0xFF) / 255.0f,
|
||||
( hex & 0xFF) / 255.0f,
|
||||
1.0f
|
||||
};
|
||||
}
|
||||
// 0xRRGGBBAA
|
||||
static constexpr Color rgba(std::uint32_t hex) {
|
||||
return {
|
||||
((hex >> 24) & 0xFF) / 255.0f,
|
||||
((hex >> 16) & 0xFF) / 255.0f,
|
||||
((hex >> 8) & 0xFF) / 255.0f,
|
||||
( hex & 0xFF) / 255.0f
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
struct Size {
|
||||
float w = 0, h = 0;
|
||||
};
|
||||
|
||||
struct Rect {
|
||||
float x = 0, y = 0, w = 0, h = 0;
|
||||
|
||||
constexpr float Right() const { return x + w; }
|
||||
constexpr float Bottom() const { return y + h; }
|
||||
|
||||
constexpr bool Contains(float px, float py) const {
|
||||
return px >= x && px < x + w && py >= y && py < y + h;
|
||||
}
|
||||
|
||||
constexpr Rect Intersect(Rect o) const {
|
||||
float l = std::max(x, o.x);
|
||||
float t = std::max(y, o.y);
|
||||
float r = std::min(Right(), o.Right());
|
||||
float b = std::min(Bottom(), o.Bottom());
|
||||
if (r <= l || b <= t) return {0, 0, 0, 0};
|
||||
return {l, t, r - l, b - t};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,110 +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"
|
||||
export module Crafter.Graphics:UIRenderer;
|
||||
import std;
|
||||
import :Device;
|
||||
import :Window;
|
||||
import :RenderPass;
|
||||
import :DescriptorHeapVulkan;
|
||||
import :VulkanBuffer;
|
||||
import :SamplerVulkan;
|
||||
import :ShaderVulkan;
|
||||
import :ImageVulkan;
|
||||
import :UIDrawList;
|
||||
import :UIAtlas;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
// The compute-pass-side renderer. Owns the compute pipeline, per-frame
|
||||
// item buffers, the SDF glyph atlas, and the descriptor-heap slot
|
||||
// allocations. Implements RenderPass so it plugs into Window::passes.
|
||||
//
|
||||
// Lifecycle:
|
||||
// - Initialize(window, shaderPath) — once, after the window has a
|
||||
// descriptor heap. Allocates slots, creates pipeline, atlas image.
|
||||
// - SetItems(span<UIItem>) per frame, before Window::Render runs.
|
||||
// - Record(...) — invoked by Window::Render's pass loop.
|
||||
class UIRenderer : public RenderPass {
|
||||
public:
|
||||
// Defaulted bindless slot capacity — covers most game UIs without
|
||||
// descriptor heap pressure. Override in Initialize.
|
||||
static constexpr std::uint16_t kDefaultBindlessImageCount = 256;
|
||||
|
||||
FontAtlas atlas;
|
||||
|
||||
// Initialize. `initCmd` must be a command buffer in recording
|
||||
// state — used to transition the atlas image. Window must already
|
||||
// have a non-null descriptorHeap with enough free slots for
|
||||
// (numFrames + 1 + bindlessImageCount) images, numFrames buffers,
|
||||
// and 1 sampler.
|
||||
void Initialize(Window& window,
|
||||
VkCommandBuffer initCmd,
|
||||
const std::filesystem::path& spvPath = "ui.comp.spv",
|
||||
std::uint16_t bindlessImageCount = kDefaultBindlessImageCount);
|
||||
|
||||
// Stage `items` into the next-frame mapped buffer. Must be called
|
||||
// BEFORE Window::Render so the buffer is flushed before the
|
||||
// dispatch reads it.
|
||||
void SetItems(std::span<const UIItem> items);
|
||||
|
||||
// RenderPass impl — invoked from Window::Render's pass loop.
|
||||
void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override;
|
||||
|
||||
// Heap slot accessors — UIScene reads these to populate DrawList.
|
||||
std::uint32_t BindlessBaseHeapIdx() const { return bindlessBase_; }
|
||||
FontAtlas& Atlas() { return atlas; }
|
||||
|
||||
// The frame currently being staged. Window::Render advances
|
||||
// `currentBuffer` before passes record; SetItems writes to
|
||||
// (currentBuffer + 1) so the previous frame's buffer is still in
|
||||
// flight on the GPU. For V1 we ride on Window's currentBuffer
|
||||
// directly since vkQueueWaitIdle gates each frame.
|
||||
std::uint32_t pendingItemCount = 0;
|
||||
|
||||
private:
|
||||
Window* window_ = nullptr;
|
||||
|
||||
VkPipeline pipeline_ = VK_NULL_HANDLE;
|
||||
|
||||
VulkanBuffer<UIItem, true> itemBufs_[Window::numFrames];
|
||||
std::uint16_t itemCapacity_ = 0;
|
||||
|
||||
// Heap slot allocations (resource heap unless noted).
|
||||
std::uint16_t outImageBase_ = 0; // images[outImageBase_ + frame] = swapchain view
|
||||
std::uint16_t atlasImageSlot_ = 0; // sampled atlas image slot
|
||||
std::uint16_t bindlessBase_ = 0; // first user-image slot
|
||||
std::uint16_t bindlessCount_ = 0; // user-image slot count
|
||||
std::uint16_t itemBufBase_ = 0; // SSBO slot base; per-frame at base + i
|
||||
std::uint16_t linearSamplerSlot_ = 0; // sampler heap
|
||||
|
||||
// Stable VkImageViewCreateInfo for the atlas — descriptor heap
|
||||
// writes need a pointer to one, so we keep it on the renderer.
|
||||
VkImageViewCreateInfo atlasViewCreateInfo_{};
|
||||
|
||||
// Helpers.
|
||||
void GrowItemBuffersIfNeeded(std::uint32_t needed);
|
||||
void WriteSwapchainDescriptors();
|
||||
void WriteAtlasDescriptor();
|
||||
void WriteSamplerDescriptors();
|
||||
void WriteItemBufferDescriptors();
|
||||
void CreatePipeline(const std::filesystem::path& spvPath);
|
||||
void CreateLinearSampler();
|
||||
};
|
||||
}
|
||||
|
|
@ -1,112 +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"
|
||||
export module Crafter.Graphics:UIScene;
|
||||
import std;
|
||||
import :Window;
|
||||
import :Types;
|
||||
import :DescriptorHeapVulkan;
|
||||
import Crafter.Event;
|
||||
import :UIWidget;
|
||||
import :UIWidgets;
|
||||
import :UILayout;
|
||||
import :UIDrawList;
|
||||
import :UIRenderer;
|
||||
import :UIHit;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
// The single user-facing wrapper that ties the widget tree to the
|
||||
// window's frame loop. Owns the renderer + draw list, optionally
|
||||
// owns a default descriptor heap, registers itself as a RenderPass on
|
||||
// the window, and routes mouse clicks through the hit tester.
|
||||
//
|
||||
// Typical usage:
|
||||
//
|
||||
// Crafter::Window window(1280, 720, "Demo");
|
||||
// window.StartInit(); window.FinishInit();
|
||||
// Crafter::UI::UIScene scene;
|
||||
// scene.Initialize(window);
|
||||
// scene.Root(VStack{}.children(
|
||||
// Button{"Play"}.onClick([&]{ ... }),
|
||||
// ...
|
||||
// ));
|
||||
// window.Render();
|
||||
// window.StartUpdate(); // continuous rendering
|
||||
// window.StartSync();
|
||||
class UIScene {
|
||||
public:
|
||||
UIRenderer renderer;
|
||||
DrawList drawList;
|
||||
|
||||
UIScene() = default;
|
||||
UIScene(const UIScene&) = delete;
|
||||
UIScene& operator=(const UIScene&) = delete;
|
||||
~UIScene();
|
||||
|
||||
void Initialize(Window& window,
|
||||
const std::filesystem::path& spvPath = "ui.comp.spv");
|
||||
|
||||
// Replace the widget tree. Takes ownership and clears focus
|
||||
// (the previously-focused widget will be destroyed with the
|
||||
// old tree).
|
||||
template<typename W>
|
||||
requires std::derived_from<std::remove_cvref_t<W>, Widget>
|
||||
void Root(W&& root) {
|
||||
SetFocus(nullptr);
|
||||
using T = std::remove_cvref_t<W>;
|
||||
auto p = std::make_unique<T>(std::move(root));
|
||||
p->parent = nullptr;
|
||||
root_ = std::move(p);
|
||||
}
|
||||
|
||||
// Focus management. Calling with nullptr blurs whatever was focused.
|
||||
void SetFocus(Widget* w);
|
||||
Widget* Focused() const { return focused_; }
|
||||
|
||||
// Optional surface-clearing colour. The swapchain image is
|
||||
// STORAGE-only (can't be vkCmdClearColorImage'd), so we paint a
|
||||
// full-surface rect at the start of every frame's draw list when
|
||||
// this is set.
|
||||
UIScene& background(Color c) { background_ = c; return *this; }
|
||||
|
||||
Widget* root() { return root_.get(); }
|
||||
const Widget* root() const { return root_.get(); }
|
||||
|
||||
private:
|
||||
Window* window_ = nullptr;
|
||||
std::unique_ptr<Widget> root_;
|
||||
std::optional<Color> background_;
|
||||
|
||||
// Auto-allocated heap for UI-only apps. If the user already attached
|
||||
// a heap to the window, we leave it alone and don't own one.
|
||||
DescriptorHeapVulkan ownedHeap_;
|
||||
bool ownsHeap_ = false;
|
||||
|
||||
std::unique_ptr<EventListener<void>> mouseListener_;
|
||||
std::unique_ptr<EventListener<FrameTime>> updateListener_;
|
||||
std::unique_ptr<EventListener<const std::string_view>> textListener_;
|
||||
std::unique_ptr<EventListener<CrafterKeys>> keyListener_;
|
||||
Widget* focused_ = nullptr;
|
||||
float elapsedSec_ = 0.0f;
|
||||
|
||||
float WindowScale() const;
|
||||
void RebuildFrame();
|
||||
};
|
||||
}
|
||||
|
|
@ -1,80 +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
|
||||
*/
|
||||
export module Crafter.Graphics:UITheme;
|
||||
import std;
|
||||
import :UILength;
|
||||
import :UIWidgets;
|
||||
import :Font;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
// Flat theme — named slots, no cascading. Users keep one Theme value
|
||||
// (typically as a member of their scene) and reference its slots on
|
||||
// each widget via `.style(theme.primary)` etc. No automatic
|
||||
// propagation: per-widget overrides win.
|
||||
struct Theme {
|
||||
// Buttons
|
||||
ButtonStyle primary; // default action ("Save", "Play")
|
||||
ButtonStyle secondary; // neutral action ("Cancel", "Back")
|
||||
ButtonStyle danger; // destructive ("Delete", "Quit")
|
||||
ButtonStyle disabled; // greyed out
|
||||
|
||||
// Inputs
|
||||
InputFieldStyle input;
|
||||
|
||||
// Generic palette
|
||||
Color text {0.95f, 0.95f, 0.95f, 1.0f};
|
||||
Color textMuted {0.65f, 0.65f, 0.65f, 1.0f};
|
||||
Color panel {0.10f, 0.11f, 0.13f, 1.0f};
|
||||
Color panelElevated {0.14f, 0.15f, 0.17f, 1.0f};
|
||||
Color border {0.30f, 0.30f, 0.30f, 1.0f};
|
||||
Color focusRing {0.40f, 0.70f, 1.00f, 1.0f};
|
||||
|
||||
// Typography. Optional: not every widget requires the theme's font;
|
||||
// builder methods can override per-instance.
|
||||
Font* defaultFont = nullptr;
|
||||
float defaultFontSize = 16.0f;
|
||||
};
|
||||
|
||||
namespace themes {
|
||||
// A balanced dark-mode theme — matches the kind of game-menu palette
|
||||
// 3DForts uses. Users can copy + tweak.
|
||||
inline Theme default_dark() {
|
||||
Theme t;
|
||||
|
||||
t.primary.background = Color{0.22f, 0.45f, 0.78f, 1.0f};
|
||||
t.primary.hoverBackground = Color{0.28f, 0.55f, 0.92f, 1.0f};
|
||||
t.primary.pressedBackground = Color{0.16f, 0.36f, 0.66f, 1.0f};
|
||||
t.primary.textColor = Color{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
|
||||
t.secondary.background = Color{0.20f, 0.20f, 0.20f, 1.0f};
|
||||
t.secondary.hoverBackground = Color{0.28f, 0.28f, 0.28f, 1.0f};
|
||||
t.secondary.pressedBackground = Color{0.14f, 0.14f, 0.14f, 1.0f};
|
||||
|
||||
t.danger.background = Color{0.62f, 0.20f, 0.20f, 1.0f};
|
||||
t.danger.hoverBackground = Color{0.78f, 0.26f, 0.26f, 1.0f};
|
||||
t.danger.pressedBackground = Color{0.46f, 0.14f, 0.14f, 1.0f};
|
||||
t.danger.textColor = Color{1.0f, 0.95f, 0.95f, 1.0f};
|
||||
|
||||
t.disabled.background = Color{0.15f, 0.15f, 0.15f, 1.0f};
|
||||
t.disabled.textColor = Color{0.50f, 0.50f, 0.50f, 1.0f};
|
||||
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,189 +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
|
||||
*/
|
||||
export module Crafter.Graphics:UIWidget;
|
||||
import std;
|
||||
import :UILength;
|
||||
import :Types; // for CrafterKeys
|
||||
|
||||
export namespace Crafter::UI {
|
||||
struct DrawList; // forward decl (full def in :UIDrawList)
|
||||
|
||||
// Threaded through layout. Holds anything every widget needs from the
|
||||
// surrounding scene at layout time (DPI scale, root surface size, …).
|
||||
struct LayoutContext {
|
||||
float scale = 1.0f; // device scale (Window::scale)
|
||||
Size surfaceSize{}; // root surface in device px
|
||||
};
|
||||
|
||||
struct Widget {
|
||||
Length width_ = Length::Auto();
|
||||
Length height_ = Length::Auto();
|
||||
Edges padding_;
|
||||
Edges margin_;
|
||||
std::optional<Anchor> anchor_;
|
||||
|
||||
// Layout output, filled by the engine.
|
||||
Rect computedRect{};
|
||||
Size desiredSize{};
|
||||
bool dirty = true;
|
||||
|
||||
// Tree.
|
||||
Widget* parent = nullptr;
|
||||
std::vector<std::unique_ptr<Widget>> children_;
|
||||
|
||||
Widget() = default;
|
||||
Widget(const Widget&) = delete;
|
||||
Widget& operator=(const Widget&) = delete;
|
||||
Widget(Widget&&) = default;
|
||||
Widget& operator=(Widget&&) = default;
|
||||
virtual ~Widget() = default;
|
||||
|
||||
// Layout protocol — Measure returns the size this widget wants given
|
||||
// the available space; engine then calls Arrange with the final rect.
|
||||
virtual Size Measure(Size avail, const LayoutContext& ctx) = 0;
|
||||
virtual void Arrange(Rect rect, const LayoutContext& ctx) = 0;
|
||||
|
||||
// Interaction protocol — return true if the event was handled and
|
||||
// should NOT bubble to the parent. Default: not handled.
|
||||
virtual bool OnMouseClick(float /*x*/, float /*y*/) { return false; }
|
||||
|
||||
// Focus protocol. Widgets that opt in (e.g. InputField) return
|
||||
// true from IsFocusable; UIScene tracks the currently-focused
|
||||
// widget and routes keyboard events to it.
|
||||
virtual bool IsFocusable() const { return false; }
|
||||
virtual void OnFocus() {}
|
||||
virtual void OnBlur() {}
|
||||
|
||||
// Keyboard input. Both default to "not handled". OnTextInput
|
||||
// receives a UTF-8 substring (typically one codepoint per call).
|
||||
// OnKeyDown receives non-character keys (Backspace, arrows, …).
|
||||
virtual bool OnTextInput(std::string_view /*text*/) { return false; }
|
||||
virtual bool OnKeyDown (CrafterKeys /*key*/) { return false; }
|
||||
|
||||
// Drawing protocol — emit GPU-bound draw items into `dl`. Default
|
||||
// implementation is "container behaviour": just descend into
|
||||
// children. Leaf widgets override to emit their own primitives;
|
||||
// containers that also draw (Button background, ScrollView clip
|
||||
// push/pop, TabView bar) override and explicitly recurse into
|
||||
// children where appropriate.
|
||||
//
|
||||
// The body just forwards to children, so the forward-declared
|
||||
// DrawList is enough — no member access here.
|
||||
virtual void Emit(DrawList& dl) const {
|
||||
for (auto& c : children_) c->Emit(dl);
|
||||
}
|
||||
|
||||
// Walk all descendants in pre-order.
|
||||
template<typename F>
|
||||
void ForEach(F&& f) {
|
||||
f(*this);
|
||||
for (auto& c : children_) c->ForEach(f);
|
||||
}
|
||||
};
|
||||
|
||||
// CRTP base providing fluent setters that return the concrete widget type.
|
||||
template<typename Self>
|
||||
struct WidgetBuilder : Widget {
|
||||
Self& self() { return static_cast<Self&>(*this); }
|
||||
|
||||
Self& width(Length l) { width_ = l; return self(); }
|
||||
Self& height(Length l) { height_ = l; return self(); }
|
||||
Self& size(Length w, Length h) { width_ = w; height_ = h; return self(); }
|
||||
Self& padding(Edges e) { padding_ = e; return self(); }
|
||||
Self& padding(float all) { padding_ = Edges(all); return self(); }
|
||||
Self& padding(float v, float h) { padding_ = Edges(v, h); return self(); }
|
||||
Self& margin(Edges e) { margin_ = e; return self(); }
|
||||
Self& margin(float all) { margin_ = Edges(all); return self(); }
|
||||
Self& anchor(Anchor a) { anchor_ = a; return self(); }
|
||||
Self& expand() { width_ = Length::Frac(1); height_ = Length::Frac(1); return self(); }
|
||||
|
||||
// Take ownership of a parameter pack of widgets and append them as children.
|
||||
template<typename... Ws>
|
||||
requires (std::derived_from<std::decay_t<Ws>, Widget> && ...)
|
||||
Self& children(Ws&&... ws) {
|
||||
children_.reserve(children_.size() + sizeof...(Ws));
|
||||
(AppendChild(std::forward<Ws>(ws)), ...);
|
||||
return self();
|
||||
}
|
||||
|
||||
private:
|
||||
// .children(...) takes ownership of each widget argument unconditionally;
|
||||
// builder chains like `Button{"X"}.font(f)` return Self& (lvalue ref to
|
||||
// the temporary), so we always move rather than std::forward.
|
||||
template<typename W>
|
||||
void AppendChild(W&& w) {
|
||||
using T = std::remove_cvref_t<W>;
|
||||
auto p = std::make_unique<T>(std::move(w));
|
||||
p->parent = this;
|
||||
children_.push_back(std::move(p));
|
||||
}
|
||||
};
|
||||
|
||||
// Stable typed handle into the scene; populated by the scene when a
|
||||
// widget tree is mounted.
|
||||
template<typename T>
|
||||
struct WidgetRef {
|
||||
T* node = nullptr;
|
||||
|
||||
T* operator->() const { return node; }
|
||||
T& operator*() const { return *node; }
|
||||
explicit operator bool() const { return node != nullptr; }
|
||||
};
|
||||
|
||||
// Mutable observable value. Setting a new value invokes any registered
|
||||
// watchers; widgets register watchers in their mount step to mark
|
||||
// themselves dirty when the underlying value changes.
|
||||
template<typename T>
|
||||
class Observable {
|
||||
public:
|
||||
Observable() = default;
|
||||
Observable(T v) : value_(std::move(v)) {}
|
||||
|
||||
Observable(const Observable&) = delete;
|
||||
Observable& operator=(const Observable&) = delete;
|
||||
|
||||
Observable& operator=(T v) {
|
||||
if constexpr (std::equality_comparable<T>) {
|
||||
if (value_ == v) return *this;
|
||||
}
|
||||
value_ = std::move(v);
|
||||
Notify();
|
||||
return *this;
|
||||
}
|
||||
|
||||
const T& Get() const { return value_; }
|
||||
operator const T&() const { return value_; }
|
||||
|
||||
// Register a watcher; returned token unregisters on destruction.
|
||||
// For V1 there is no unsubscribe — watchers live as long as the
|
||||
// Observable does. The scene clears watchers when widgets are torn
|
||||
// down by destroying the Observable they were watching.
|
||||
void Watch(std::function<void()> fn) {
|
||||
watchers_.push_back(std::move(fn));
|
||||
}
|
||||
|
||||
private:
|
||||
T value_{};
|
||||
std::vector<std::function<void()>> watchers_;
|
||||
|
||||
void Notify() {
|
||||
for (auto& w : watchers_) w();
|
||||
}
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -106,9 +106,19 @@ export namespace Crafter {
|
|||
void Resize(std::uint32_t width, std::uint32_t height);
|
||||
void Render();
|
||||
void Update();
|
||||
void UpdateCursorImage();
|
||||
void SetCusorImage(std::uint16_t sizeX, std::uint16_t sizeY);
|
||||
void SetCusorImageDefault();
|
||||
// Replace the system cursor with a custom image. `pixels` is
|
||||
// `width*height*4` bytes in R8G8B8A8 memory order (matching
|
||||
// stb_image's STBI_rgb_alpha output) with straight (non-premultiplied)
|
||||
// alpha — the conversion to the compositor's expected format is
|
||||
// handled internally. The hotspot is in image-pixel coordinates.
|
||||
// Re-callable at any time.
|
||||
void SetCursorImage(std::uint16_t width, std::uint16_t height,
|
||||
std::uint16_t hotspotX, std::uint16_t hotspotY,
|
||||
const std::uint8_t* pixels);
|
||||
|
||||
// Restore the default system cursor (releases any previously-uploaded
|
||||
// cursor pixel buffer).
|
||||
void SetDefaultCursor();
|
||||
|
||||
#ifdef CRAFTER_TIMING
|
||||
std::chrono::nanoseconds totalUpdate;
|
||||
|
|
@ -139,6 +149,15 @@ export namespace Crafter {
|
|||
wl_surface* cursorSurface = nullptr;
|
||||
wl_buffer* cursorWlBuffer = nullptr;
|
||||
std::uint32_t cursorBufferOldSize = 0;
|
||||
// mmap'd view of the SHM cursor buffer — the user-supplied pixels
|
||||
// are written here in BGRA8888 order. Lifetime matches cursorWlBuffer.
|
||||
std::uint8_t* cursorMmap_ = nullptr;
|
||||
std::uint16_t cursorHotspotX_ = 0;
|
||||
std::uint16_t cursorHotspotY_ = 0;
|
||||
// Most recent serial from a wl_pointer.enter on this window's surface.
|
||||
// Needed so `SetCursorImage` can re-issue `wl_pointer_set_cursor`
|
||||
// mid-session (the hotspot only updates when set_cursor is recalled).
|
||||
std::uint32_t lastPointerSerial_ = 0;
|
||||
|
||||
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
|
||||
static void wl_surface_frame_done(void *data, wl_callback *cb, uint32_t time);
|
||||
|
|
|
|||
|
|
@ -38,4 +38,7 @@ export import :SamplerVulkan;
|
|||
export import :DescriptorHeapVulkan;
|
||||
export import :RenderPass;
|
||||
export import :RTPass;
|
||||
export import :UI;
|
||||
export import :FontAtlas;
|
||||
export import :ComputeShader;
|
||||
export import :UI;
|
||||
export import :UIComponents;
|
||||
Loading…
Add table
Add a link
Reference in a new issue