new UI system
This commit is contained in:
parent
d840a81448
commit
216972e73a
82 changed files with 4837 additions and 3243 deletions
|
|
@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
*/
|
||||
|
||||
module;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
export module Crafter.Graphics:DescriptorHeapVulkan;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
import std;
|
||||
import :Device;
|
||||
import :Window;
|
||||
|
|
@ -30,12 +27,23 @@ import :Types;
|
|||
import :VulkanBuffer;
|
||||
|
||||
export namespace Crafter {
|
||||
struct ImageSlotRange { std::uint16_t firstElement; std::uint16_t count; };
|
||||
struct BufferSlotRange { std::uint16_t firstElement; std::uint16_t count; };
|
||||
struct SamplerSlotRange { std::uint16_t firstElement; std::uint16_t count; };
|
||||
|
||||
struct DescriptorHeapVulkan {
|
||||
VulkanBuffer<std::uint8_t, true> resourceHeap[Window::numFrames];
|
||||
VulkanBuffer<std::uint8_t, true> samplerHeap[Window::numFrames];
|
||||
std::uint32_t bufferStartOffset;
|
||||
std::uint16_t bufferStartElement;
|
||||
|
||||
|
||||
std::uint16_t imageCapacity = 0;
|
||||
std::uint16_t bufferCapacity = 0;
|
||||
std::uint16_t samplerCapacity = 0;
|
||||
std::uint16_t imageNext = 0;
|
||||
std::uint16_t bufferNext = 0;
|
||||
std::uint16_t samplerNext = 0;
|
||||
|
||||
void Initialize(std::uint16_t images, std::uint16_t buffers, std::uint16_t samplers) {
|
||||
std::uint32_t descriptorRegion = images * Device::descriptorHeapProperties.imageDescriptorSize + buffers * Device::descriptorHeapProperties.bufferDescriptorSize;
|
||||
std::uint32_t alignedDescriptorRegion = (descriptorRegion + Device::descriptorHeapProperties.imageDescriptorAlignment - 1) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1);
|
||||
|
|
@ -47,11 +55,57 @@ export namespace Crafter {
|
|||
bufferStartElement = 1;
|
||||
}
|
||||
bufferStartOffset = bufferStartElement * Device::descriptorHeapProperties.bufferDescriptorSize;
|
||||
|
||||
imageCapacity = images;
|
||||
bufferCapacity = buffers;
|
||||
samplerCapacity = samplers;
|
||||
imageNext = 0;
|
||||
bufferNext = 0;
|
||||
samplerNext = 0;
|
||||
|
||||
for(std::uint8_t i = 0; i < Window::numFrames; i++) {
|
||||
resourceHeap[i].Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, resourceSize);
|
||||
samplerHeap[i].Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, samplerSize);
|
||||
}
|
||||
}
|
||||
|
||||
ImageSlotRange AllocateImageSlots(std::uint16_t count) {
|
||||
if (imageNext + count > imageCapacity) {
|
||||
throw std::runtime_error(std::format("DescriptorHeapVulkan: out of image slots ({} requested, {} remaining of {})", count, imageCapacity - imageNext, imageCapacity));
|
||||
}
|
||||
ImageSlotRange r{imageNext, count};
|
||||
imageNext += count;
|
||||
return r;
|
||||
}
|
||||
|
||||
BufferSlotRange AllocateBufferSlots(std::uint16_t count) {
|
||||
if (bufferNext + count > bufferCapacity) {
|
||||
throw std::runtime_error(std::format("DescriptorHeapVulkan: out of buffer slots ({} requested, {} remaining of {})", count, bufferCapacity - bufferNext, bufferCapacity));
|
||||
}
|
||||
BufferSlotRange r{bufferNext, count};
|
||||
bufferNext += count;
|
||||
return r;
|
||||
}
|
||||
|
||||
SamplerSlotRange AllocateSamplerSlots(std::uint16_t count) {
|
||||
if (samplerNext + count > samplerCapacity) {
|
||||
throw std::runtime_error(std::format("DescriptorHeapVulkan: out of sampler slots ({} requested, {} remaining of {})", count, samplerCapacity - samplerNext, samplerCapacity));
|
||||
}
|
||||
SamplerSlotRange r{samplerNext, count};
|
||||
samplerNext += count;
|
||||
return r;
|
||||
}
|
||||
|
||||
std::uint32_t ImageByteOffset(std::uint16_t firstElement) const {
|
||||
return firstElement * Device::descriptorHeapProperties.imageDescriptorSize;
|
||||
}
|
||||
std::uint32_t BufferByteOffset(std::uint16_t firstElement) const {
|
||||
return bufferStartOffset + firstElement * Device::descriptorHeapProperties.bufferDescriptorSize;
|
||||
}
|
||||
std::uint32_t SamplerByteOffset(std::uint16_t firstElement) const {
|
||||
return firstElement * Device::descriptorHeapProperties.samplerDescriptorSize;
|
||||
}
|
||||
|
||||
inline static std::uint32_t GetBufferOffset(std::uint16_t images, std::uint16_t buffers) {
|
||||
std::uint32_t bufferStartElement = images * Device::descriptorHeapProperties.imageDescriptorSize / Device::descriptorHeapProperties.bufferDescriptorSize;
|
||||
|
||||
|
|
@ -66,10 +120,8 @@ export namespace Crafter {
|
|||
if(images > 0 && bufferStartElement == 0) {
|
||||
bufferStartElement = 1;
|
||||
}
|
||||
|
||||
|
||||
return bufferStartElement;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
|
@ -18,9 +18,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
*/
|
||||
|
||||
module;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
#include <wayland-client.h>
|
||||
#include <wayland-client-protocol.h>
|
||||
|
|
@ -98,7 +96,6 @@ export namespace Crafter {
|
|||
};
|
||||
#endif
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
inline static VkInstance instance = VK_NULL_HANDLE;
|
||||
inline static VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE;
|
||||
inline static VkPhysicalDevice physDevice = VK_NULL_HANDLE;
|
||||
|
|
@ -117,6 +114,8 @@ export namespace Crafter {
|
|||
inline static PFN_vkCmdBindResourceHeapEXT vkCmdBindResourceHeapEXT;
|
||||
inline static PFN_vkCmdBindSamplerHeapEXT vkCmdBindSamplerHeapEXT;
|
||||
inline static PFN_vkWriteResourceDescriptorsEXT vkWriteResourceDescriptorsEXT;
|
||||
inline static PFN_vkWriteSamplerDescriptorsEXT vkWriteSamplerDescriptorsEXT;
|
||||
inline static PFN_vkCmdPushDataEXT vkCmdPushDataEXT;
|
||||
inline static PFN_vkGetPhysicalDeviceDescriptorSizeEXT vkGetPhysicalDeviceDescriptorSizeEXT;
|
||||
inline static PFN_vkGetDeviceFaultInfoEXT vkGetDeviceFaultInfoEXT;
|
||||
|
||||
|
|
@ -132,6 +131,5 @@ export namespace Crafter {
|
|||
|
||||
static void CheckVkResult(VkResult result);
|
||||
static std::uint32_t GetMemoryType(std::uint32_t typeBits, VkMemoryPropertyFlags properties);
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
|
@ -26,14 +26,45 @@ export module Crafter.Graphics:Font;
|
|||
import std;
|
||||
|
||||
namespace Crafter {
|
||||
// Decode the UTF-8 codepoint at `text[i]` and advance `i` past it.
|
||||
// Returns 0 once `i` reaches the end. Malformed sequences yield U+FFFD
|
||||
// and the index is moved past one byte to keep iteration finite.
|
||||
export inline std::uint32_t DecodeUtf8(std::string_view text, std::size_t& i) {
|
||||
if (i >= text.size()) return 0;
|
||||
std::uint8_t b0 = static_cast<std::uint8_t>(text[i]);
|
||||
|
||||
// Single-byte ASCII is the common path.
|
||||
if (b0 < 0x80) { ++i; return b0; }
|
||||
|
||||
int extra;
|
||||
std::uint32_t cp;
|
||||
if ((b0 & 0xE0) == 0xC0) { extra = 1; cp = b0 & 0x1F; }
|
||||
else if ((b0 & 0xF0) == 0xE0) { extra = 2; cp = b0 & 0x0F; }
|
||||
else if ((b0 & 0xF8) == 0xF0) { extra = 3; cp = b0 & 0x07; }
|
||||
else { ++i; return 0xFFFD; } // continuation byte at start, or 5+-byte leader
|
||||
|
||||
++i;
|
||||
for (int k = 0; k < extra; ++k) {
|
||||
if (i >= text.size()) return 0xFFFD;
|
||||
std::uint8_t b = static_cast<std::uint8_t>(text[i]);
|
||||
if ((b & 0xC0) != 0x80) return 0xFFFD; // missing continuation
|
||||
cp = (cp << 6) | (b & 0x3Fu);
|
||||
++i;
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
export class Font {
|
||||
public:
|
||||
std::vector<unsigned char> fontBuffer;
|
||||
std::int_fast32_t ascent;
|
||||
std::int_fast32_t descent;
|
||||
std::int_fast32_t ascent;
|
||||
std::int_fast32_t descent;
|
||||
std::int_fast32_t lineGap;
|
||||
stbtt_fontinfo font;
|
||||
Font(const std::filesystem::path& font);
|
||||
std::uint32_t GetLineWidth(const std::string_view text, float size);
|
||||
float LineHeight(float size);
|
||||
float AscentPx(float size); // baseline offset from line-top
|
||||
float ScaleForSize(float size); // stb's pixel-units-per-em factor
|
||||
};
|
||||
}
|
||||
|
|
@ -1,64 +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:GridElement;
|
||||
import std;
|
||||
import :Transform2D;
|
||||
import :ForwardDeclarations;
|
||||
|
||||
export namespace Crafter {
|
||||
struct GridElement : Transform2D {
|
||||
std::uint32_t columns;
|
||||
std::uint32_t rows;
|
||||
std::int32_t spacingX;
|
||||
std::int32_t spacingY;
|
||||
std::int32_t paddingX;
|
||||
std::int32_t paddingY;
|
||||
GridElement(std::uint32_t columns, std::uint32_t rows, std::int32_t spacingX, std::int32_t spacingY, std::int32_t paddingX, std::int32_t paddingY, Anchor2D anchor) : Transform2D(anchor), columns(columns), rows(rows), spacingX(spacingX), spacingY(spacingY), paddingX(paddingX), paddingY(paddingY) {
|
||||
|
||||
}
|
||||
void UpdatePosition(RendertargetBase& window, Transform2D& parent) override {
|
||||
ScaleElement(parent);
|
||||
std::int32_t cellWidth = (paddingX * 2) - (spacingX * (columns - 1)) / columns;
|
||||
std::int32_t cellHeight = (paddingY * 2) - (spacingY * (rows - 1)) / rows;
|
||||
|
||||
std::size_t childIndex = 0;
|
||||
for (std::uint32_t row = 0; row < rows && childIndex < this->children.size(); ++row) {
|
||||
for (std::uint32_t col = 0; col < columns && childIndex < this->children.size(); ++col) {
|
||||
Transform2D* child = this->children[childIndex];
|
||||
|
||||
// Calculate position for this child
|
||||
std::int32_t childX = (cellWidth * col) + (spacingX * col) + paddingX;
|
||||
|
||||
std::int32_t childY = (cellHeight * row) + (spacingY * row) + paddingY;
|
||||
|
||||
// Apply relative positioning
|
||||
child->anchor.x = childX;
|
||||
child->anchor.y = childY;
|
||||
child->anchor.width = cellWidth;
|
||||
child->anchor.height = cellHeight;
|
||||
|
||||
// Update child position
|
||||
child->UpdatePosition(window, *this);
|
||||
childIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -19,16 +19,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0215-1301 USA
|
|||
|
||||
module;
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
|
||||
export module Crafter.Graphics:ImageVulkan;
|
||||
import std;
|
||||
import :VulkanBuffer;
|
||||
|
||||
export namespace Crafter {
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
template <typename PixelType>
|
||||
class ImageVulkan {
|
||||
public:
|
||||
|
|
@ -45,7 +42,11 @@ export namespace Crafter {
|
|||
this->width = width;
|
||||
this->height = height;
|
||||
this->mipLevels = mipLevels;
|
||||
buffer.Create(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, width * height);
|
||||
buffer.Create(
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
|
||||
width * height
|
||||
);
|
||||
|
||||
VkImageCreateInfo imageInfo = {};
|
||||
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
|
|
@ -178,5 +179,4 @@ export namespace Crafter {
|
|||
vkCmdPipelineBarrier(cmd, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
}
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
|
@ -19,9 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
|
||||
module;
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
|
||||
export module Crafter.Graphics:Mesh;
|
||||
import std;
|
||||
|
|
@ -29,7 +27,6 @@ import Crafter.Math;
|
|||
import :VulkanBuffer;
|
||||
|
||||
export namespace Crafter {
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
class Mesh {
|
||||
public:
|
||||
VulkanBuffer<char, false> scratchBuffer;
|
||||
|
|
@ -43,5 +40,4 @@ export namespace Crafter {
|
|||
bool opaque;
|
||||
void Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd);
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
|
@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
*/
|
||||
|
||||
module;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
export module Crafter.Graphics:PipelineRTVulkan;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
import std;
|
||||
import :Device;
|
||||
import :VulkanBuffer;
|
||||
|
|
@ -115,6 +112,4 @@ export namespace Crafter {
|
|||
vkDestroyPipeline(Device::device, pipeline, nullptr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
44
interfaces/Crafter.Graphics-RTPass.cppm
Normal file
44
interfaces/Crafter.Graphics-RTPass.cppm
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
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:RTPass;
|
||||
import std;
|
||||
import :RenderPass;
|
||||
import :Window;
|
||||
import :Device;
|
||||
import :PipelineRTVulkan;
|
||||
|
||||
export namespace Crafter {
|
||||
struct RTPass : RenderPass {
|
||||
PipelineRTVulkan* pipeline;
|
||||
|
||||
RTPass(PipelineRTVulkan* p) : pipeline(p) {}
|
||||
|
||||
void Record(VkCommandBuffer cmd, std::uint32_t /*frameIdx*/, Window& window) override {
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline);
|
||||
Device::vkCmdTraceRaysKHR(cmd,
|
||||
&pipeline->raygenRegion,
|
||||
&pipeline->missRegion,
|
||||
&pipeline->hitRegion,
|
||||
&pipeline->callableRegion,
|
||||
window.width, window.height, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -16,29 +16,16 @@ 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:MouseElement;
|
||||
module;
|
||||
#include "vulkan/vulkan.h"
|
||||
export module Crafter.Graphics:RenderPass;
|
||||
import std;
|
||||
import Crafter.Event;
|
||||
import :Transform2D;
|
||||
import :ForwardDeclarations;
|
||||
|
||||
export namespace Crafter {
|
||||
struct MouseElement : Transform2D {
|
||||
Event<void> onMouseMove;
|
||||
Event<void> onMouseEnter;
|
||||
Event<void> onMouseLeave;
|
||||
Event<void> onMouseRightClick;
|
||||
Event<void> onMouseLeftClick;
|
||||
Event<void> onMouseRightHold;
|
||||
Event<void> onMouseLeftHold;
|
||||
Event<void> onMouseRightRelease;
|
||||
Event<void> onMouseLeftRelease;
|
||||
bool mouseHover = false;
|
||||
struct Window;
|
||||
|
||||
MouseElement();
|
||||
MouseElement(Window& window);
|
||||
MouseElement(Anchor2D anchor);
|
||||
MouseElement(Anchor2D anchor, Window& window);
|
||||
struct RenderPass {
|
||||
virtual void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
|
||||
virtual ~RenderPass() = default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,449 +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 "../lib/stb_truetype.h"
|
||||
export module Crafter.Graphics:RenderingElement2D;
|
||||
import Crafter.Asset;
|
||||
import std;
|
||||
import :Transform2D;
|
||||
import :RenderingElement2DBase;
|
||||
import :Font;
|
||||
import :Types;
|
||||
import :Window;
|
||||
|
||||
export namespace Crafter {
|
||||
template<typename T, bool Scaling, bool Owning, bool Rotating, std::uint8_t Alignment = 0, std::uint8_t Frames = 1> requires ((!Rotating || Scaling) && (!Owning || Scaling))
|
||||
struct RenderingElement2D : RenderingElement2DBase<T, Frames>, ScalingBase<T, Scaling, Owning, Alignment>, RotatingBase<Rotating> {
|
||||
RenderingElement2D() = default;
|
||||
RenderingElement2D(Anchor2D anchor, OpaqueType opaque) : RenderingElement2DBase<T, Frames>(anchor, opaque) {
|
||||
|
||||
}
|
||||
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t rotation) requires(Rotating) : RenderingElement2DBase<T, Frames>(anchor, opaque), RotatingBase<Rotating>(rotation) {
|
||||
|
||||
}
|
||||
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector<T, 4, Alignment>* scalingBuffer) requires(Scaling && !Owning) : RenderingElement2DBase<T, Frames>(anchor, opaque), ScalingBase<T, Scaling, Owning, Alignment>(bufferWidth, bufferHeight, scalingBuffer) {
|
||||
|
||||
}
|
||||
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector<T, 4, Alignment>* scalingBuffer, std::uint32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElement2DBase<T, Frames>(anchor, opaque), ScalingBase<T, Scaling, Owning, Alignment>(bufferWidth, bufferHeight, scalingBuffer), RotatingBase<Rotating>(rotation) {
|
||||
|
||||
}
|
||||
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight) requires(Owning) : RenderingElement2DBase<T, Frames>(anchor, opaque), ScalingBase<T, Scaling, Owning, Alignment>(bufferWidth, bufferHeight) {
|
||||
|
||||
}
|
||||
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, std::uint32_t rotation) requires(Owning && Rotating) : RenderingElement2DBase<T, Frames>(anchor, opaque), ScalingBase<T, Scaling, Owning, Alignment>(bufferWidth, bufferHeight) , RotatingBase<Rotating>(rotation) {
|
||||
|
||||
}
|
||||
RenderingElement2D(Anchor2D anchor, TextureAsset<Vector<T, 4, Alignment>>& texture) requires(!Owning && Scaling) : RenderingElement2DBase<T, Frames>(anchor, texture.opaque), ScalingBase<T, Scaling, Owning, Alignment>(texture.pixels.data(), texture.sizeX, texture.sizeY) {
|
||||
|
||||
}
|
||||
RenderingElement2D(Anchor2D anchor, TextureAsset<Vector<T, 4, Alignment>>& texture, std::uint32_t rotation) requires(!Owning && Scaling && Rotating) : RenderingElement2DBase<T, Frames>(anchor, texture.opaque), ScalingBase<T, Scaling, Owning, Alignment>(texture.pixels.data(), texture.sizeX, texture.sizeY), RotatingBase<Rotating>(rotation) {
|
||||
|
||||
}
|
||||
|
||||
RenderingElement2D(RenderingElement2D&) = delete;
|
||||
RenderingElement2D& operator=(RenderingElement2D&) = delete;
|
||||
|
||||
void ScaleNearestNeighbor() requires(Scaling) {
|
||||
for (std::uint32_t y = 0; y < this->scaled.size.y; y++) {
|
||||
std::uint32_t srcY = y * ScalingBase<T, true, Owning, Alignment>::bufferHeight / this->scaled.size.y;
|
||||
for (std::uint32_t x = 0; x < this->scaled.size.x; x++) {
|
||||
std::uint32_t srcX = x * ScalingBase<T, true, Owning, Alignment>::bufferWidth / this->scaled.size.x;
|
||||
this->buffer[y * this->scaled.size.x + x] = ScalingBase<T, true, Owning, Alignment>::scalingBuffer[srcY * ScalingBase<T, true, Owning, Alignment>::bufferWidth + srcX];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScaleRotating() requires(Scaling) {
|
||||
const float dstWidth = this->scaled.size.x;
|
||||
const float dstHeight = this->scaled.size.y;
|
||||
|
||||
const float c2 = std::abs(std::cos(RotatingBase<true>::rotation));
|
||||
const float s2 = std::abs(std::sin(RotatingBase<true>::rotation));
|
||||
|
||||
const float rotatedWidth = dstWidth * c2 + dstHeight * s2;
|
||||
const float rotatedHeight = dstWidth * s2 + dstHeight * c2;
|
||||
|
||||
const float diffX = std::ceil((rotatedWidth - dstWidth) * 0.5);
|
||||
const float diffY = std::ceil((rotatedHeight - dstHeight) * 0.5);
|
||||
|
||||
this->scaled.size.x += diffX + diffX;
|
||||
this->scaled.size.y += diffY + diffY;
|
||||
|
||||
this->scaled.position.x -= diffX;
|
||||
this->scaled.position.y -= diffY;
|
||||
|
||||
this->buffer.clear();
|
||||
this->buffer.resize(this->scaled.size.x * this->scaled.size.y);
|
||||
|
||||
// Destination center
|
||||
const float dstCx = (this->scaled.size.x - 1.0) * 0.5;
|
||||
const float dstCy = (this->scaled.size.y - 1.0) * 0.5;
|
||||
|
||||
// Source center
|
||||
const float srcCx = (ScalingBase<T, true, Owning, Alignment>::bufferWidth - 1.0) * 0.5;
|
||||
const float srcCy = (ScalingBase<T, true, Owning, Alignment>::bufferHeight - 1.0) * 0.5;
|
||||
|
||||
const float c = std::cos(RotatingBase<true>::rotation);
|
||||
const float s = std::sin(RotatingBase<true>::rotation);
|
||||
|
||||
// Scale factors (destination → source)
|
||||
const float scaleX = static_cast<float>(ScalingBase<T, true, Owning, Alignment>::bufferWidth) / dstWidth;
|
||||
const float scaleY = static_cast<float>(ScalingBase<T, true, Owning, Alignment>::bufferHeight) / dstHeight;
|
||||
|
||||
for (std::uint32_t yB = 0; yB < this->scaled.size.y; ++yB) {
|
||||
for (std::uint32_t xB = 0; xB < this->scaled.size.x; ++xB) {
|
||||
|
||||
// ---- Destination pixel relative to center ----
|
||||
const float dx = (static_cast<float>(xB) - dstCx) * scaleX;
|
||||
const float dy = (static_cast<float>(yB) - dstCy) * scaleY;
|
||||
|
||||
// ---- Inverse rotation ----
|
||||
const float sx = (c * dx - s * dy) + srcCx;
|
||||
const float sy = (s * dx + c * dy) + srcCy;
|
||||
|
||||
// ---- Nearest neighbour sampling ----
|
||||
const std::int32_t srcX = static_cast<std::int32_t>(std::round(sx));
|
||||
const std::int32_t srcY = static_cast<std::int32_t>(std::round(sy));
|
||||
|
||||
if (srcX >= 0 && srcX < ScalingBase<T, true, Owning, Alignment>::bufferWidth && srcY >= 0 && srcY < ScalingBase<T, true, Owning, Alignment>::bufferHeight) {
|
||||
this->buffer[yB * this->scaled.size.x + xB] = ScalingBase<T, true, Owning, Alignment>::scalingBuffer[srcY * ScalingBase<T, true, Owning, Alignment>::bufferWidth + srcX];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void UpdatePosition(RendertargetBase& window, Transform2D& parent) override {
|
||||
ScaleData2D oldScale = this->scaled;
|
||||
this->ScaleElement(parent);
|
||||
if constexpr(Scaling && !Rotating) {
|
||||
if(oldScale.size.x != this->scaled.size.x || oldScale.size.y != this->scaled.size.y) {
|
||||
this->buffer.resize(this->scaled.size.x * this->scaled.size.y);
|
||||
ScaleNearestNeighbor();
|
||||
}
|
||||
} else if constexpr(Rotating) {
|
||||
if(oldScale.size.x != this->scaled.size.x || oldScale.size.y != this->scaled.size.y) {
|
||||
this->buffer.resize(this->scaled.size.x * this->scaled.size.y);
|
||||
ScaleRotating();
|
||||
}
|
||||
} else {
|
||||
if(oldScale.size.x != this->scaled.size.x || oldScale.size.y != this->scaled.size.y) {
|
||||
this->buffer.resize(this->scaled.size.x * this->scaled.size.y);
|
||||
}
|
||||
}
|
||||
for(Transform2D* child : this->children) {
|
||||
child->UpdatePosition(window, *this);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string_view> ResizeText(RendertargetBase& window, Transform2D& parent, const std::string_view text, float& size, Font& font, TextOverflowMode overflowMode = TextOverflowMode::Clip, TextScaleMode scaleMode = TextScaleMode::None) {
|
||||
float scale = stbtt_ScaleForPixelHeight(&font.font, size);
|
||||
int baseline = (int)(font.ascent * scale);
|
||||
|
||||
std::vector<std::string_view> lines;
|
||||
std::string_view remaining = text;
|
||||
|
||||
std::uint32_t lineHeight = (font.ascent - font.descent) * scale;
|
||||
|
||||
if(overflowMode == TextOverflowMode::Clip) {
|
||||
while (!remaining.empty()) {
|
||||
// Find next newline or end of string
|
||||
auto newlinePos = remaining.find('\n');
|
||||
if (newlinePos != std::string_view::npos) {
|
||||
lines.emplace_back(remaining.substr(0, newlinePos));
|
||||
remaining = remaining.substr(newlinePos + 1);
|
||||
} else {
|
||||
lines.emplace_back(remaining);
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::uint32_t maxWidth = 0;
|
||||
|
||||
for(const std::string_view line: lines) {
|
||||
std::uint32_t lineWidth = 0;
|
||||
for (const char c : line) {
|
||||
int advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb);
|
||||
lineWidth += (int)(advance * scale);
|
||||
}
|
||||
if(lineWidth > maxWidth) {
|
||||
maxWidth = lineWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if(scaleMode == TextScaleMode::Element) {
|
||||
std::int32_t logicalPerPixelY = this->anchor.height / this->scaled.size.y;
|
||||
std::int32_t oldHeight = this->anchor.height;
|
||||
std::int32_t logicalPerPixelX = this->anchor.width / this->scaled.size.x;
|
||||
std::int32_t oldwidth = this->anchor.width;
|
||||
this->anchor.height = lineHeight * logicalPerPixelY;
|
||||
this->anchor.width = maxWidth * logicalPerPixelX;
|
||||
if(oldHeight != this->anchor.height || oldwidth != this->anchor.width) {
|
||||
UpdatePosition(window, parent);
|
||||
}
|
||||
} else if(scaleMode == TextScaleMode::Font) {
|
||||
float lineHeightPerFont = lineHeight / size;
|
||||
float lineWidthPerFont = maxWidth / size;
|
||||
|
||||
float maxFontHeight = this->scaled.size.y / lineHeightPerFont;
|
||||
float maxFontWidth = this->scaled.size.x / lineWidthPerFont;
|
||||
|
||||
size = std::min(maxFontHeight, maxFontWidth);
|
||||
} else {
|
||||
if constexpr(Scaling) {
|
||||
lines.resize(ScalingBase<T, true, Owning, Alignment>::bufferHeight / lines.size());
|
||||
} else {
|
||||
lines.resize(this->scaled.size.y / lines.size());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (!remaining.empty()) {
|
||||
std::string_view line;
|
||||
auto newlinePos = remaining.find('\n');
|
||||
if (newlinePos != std::string_view::npos) {
|
||||
line = remaining.substr(0, newlinePos);
|
||||
remaining = remaining.substr(newlinePos + 1);
|
||||
} else {
|
||||
line = remaining;
|
||||
remaining = "";
|
||||
}
|
||||
|
||||
std::uint32_t lineWidth = 0;
|
||||
std::size_t lastWrapPos = 0; // position of last space that can be used to wrap
|
||||
std::size_t startPos = 0;
|
||||
|
||||
for (std::size_t i = 0; i < line.size(); ++i) {
|
||||
char c = line[i];
|
||||
|
||||
// get width of this character
|
||||
int advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb);
|
||||
lineWidth += (std::uint32_t)(advance * scale);
|
||||
|
||||
// remember last space for wrapping
|
||||
if (c == ' ') {
|
||||
lastWrapPos = i;
|
||||
}
|
||||
|
||||
// if line exceeds width, wrap
|
||||
if (lineWidth > this->scaled.size.x) {
|
||||
std::size_t wrapPos;
|
||||
if (lastWrapPos > startPos) {
|
||||
wrapPos = lastWrapPos; // wrap at last space
|
||||
} else {
|
||||
wrapPos = i; // no space, hard wrap
|
||||
}
|
||||
|
||||
// push the line up to wrapPos
|
||||
lines.push_back(line.substr(startPos, wrapPos - startPos));
|
||||
|
||||
// skip any spaces at the beginning of next line
|
||||
startPos = wrapPos;
|
||||
while (startPos < line.size() && line[startPos] == ' ') {
|
||||
++startPos;
|
||||
}
|
||||
|
||||
// reset width and i
|
||||
lineWidth = 0;
|
||||
i = startPos - 1; // -1 because loop will increment i
|
||||
}
|
||||
}
|
||||
|
||||
// add the remaining part of the line
|
||||
if (startPos < line.size()) {
|
||||
lines.push_back(line.substr(startPos));
|
||||
}
|
||||
}
|
||||
|
||||
if(scaleMode == TextScaleMode::Element) {
|
||||
float logicalPerPixelY = this->anchor.height / this->scaled.size.y;
|
||||
float oldHeight = this->anchor.height;
|
||||
this->anchor.height = lineHeight * logicalPerPixelY;
|
||||
if(oldHeight != this->anchor.height) {
|
||||
UpdatePosition(window, parent);
|
||||
}
|
||||
} else if(scaleMode == TextScaleMode::Font) {
|
||||
float lineHeightPerFont = lineHeight / size;
|
||||
size = this->scaled.size.y / lineHeightPerFont;
|
||||
} else {
|
||||
if constexpr(Scaling) {
|
||||
lines.resize(ScalingBase<T, true, Owning, Alignment>::bufferHeight / lines.size());
|
||||
} else {
|
||||
lines.resize(this->scaled.size.y / lines.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
int utf8_decode(const char* s, int* bytes_consumed) {
|
||||
unsigned char c = s[0];
|
||||
if (c < 0x80) {
|
||||
*bytes_consumed = 1;
|
||||
return c;
|
||||
} else if ((c & 0xE0) == 0xC0) {
|
||||
*bytes_consumed = 2;
|
||||
return ((c & 0x1F) << 6) | (s[1] & 0x3F);
|
||||
} else if ((c & 0xF0) == 0xE0) {
|
||||
*bytes_consumed = 3;
|
||||
return ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F);
|
||||
} else if ((c & 0xF8) == 0xF0) {
|
||||
*bytes_consumed = 4;
|
||||
return ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F);
|
||||
}
|
||||
*bytes_consumed = 1;
|
||||
return 0xFFFD; // replacement char
|
||||
}
|
||||
|
||||
void RenderText(std::span<const std::string_view> lines, float size, Vector<T, 4> color, Font& font, TextAlignment alignment = TextAlignment::Left, std::uint32_t offsetX = 0, std::uint32_t offsetY = 0, OpaqueType opaque = OpaqueType::FullyOpaque) {
|
||||
float scale = stbtt_ScaleForPixelHeight(&font.font, size);
|
||||
int baseline = (int)(font.ascent * scale);
|
||||
std::uint32_t lineHeight = (font.ascent - font.descent) * scale;
|
||||
std::uint32_t currentY = baseline;
|
||||
for(std::string_view line : lines) {
|
||||
|
||||
std::uint32_t lineWidth = 0;
|
||||
for (const char c : line) {
|
||||
int advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb);
|
||||
lineWidth += (int)(advance * scale);
|
||||
}
|
||||
|
||||
std::uint32_t x = 0;
|
||||
switch (alignment) {
|
||||
case TextAlignment::Left:
|
||||
x = 0;
|
||||
break;
|
||||
case TextAlignment::Center:
|
||||
x = (this->scaled.size.x - lineWidth) / 2;
|
||||
break;
|
||||
case TextAlignment::Right:
|
||||
x = this->scaled.size.x - lineWidth;
|
||||
break;
|
||||
}
|
||||
|
||||
const char* p = line.data();
|
||||
const char* end = p + line.size();
|
||||
|
||||
while (p < end) {
|
||||
int bytes;
|
||||
int codepoint = utf8_decode(p, &bytes);
|
||||
p += bytes;
|
||||
|
||||
int ax;
|
||||
int lsb;
|
||||
stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb);
|
||||
|
||||
int c_x1, c_y1, c_x2, c_y2;
|
||||
stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2);
|
||||
|
||||
int w = c_x2 - c_x1;
|
||||
int h = c_y2 - c_y1;
|
||||
|
||||
std::vector<unsigned char> bitmap(w * h);
|
||||
stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint);
|
||||
|
||||
// Only render characters that fit within the scaled bounds
|
||||
switch(opaque) {
|
||||
case OpaqueType::FullyOpaque: {
|
||||
for (int j = 0; j < h; j++) {
|
||||
for (int i = 0; i < w; i++) {
|
||||
int bufferX = x + i + c_x1 + offsetX;
|
||||
int bufferY = currentY + j + c_y1 + offsetY;
|
||||
|
||||
// Only draw pixels that are within our scaled buffer bounds
|
||||
if constexpr(Scaling) {
|
||||
if (bufferX >= 0 && bufferX < ScalingBase<T, true, Owning, Alignment>::bufferWidth && bufferY >= 0 && bufferY < ScalingBase<T, true, Owning, Alignment>::bufferHeight) {
|
||||
ScalingBase<T, true, Owning, Alignment>::scalingBuffer[bufferY * ScalingBase<T, true, Owning, Alignment>::bufferWidth + bufferX] = {color.r, color.g, color.b, static_cast<T>(bitmap[j * w + i])};
|
||||
}
|
||||
} else {
|
||||
if (bufferX >= 0 && bufferX < (int)this->scaled.size.x && bufferY >= 0 && bufferY < (int)this->scaled.size.y) {
|
||||
this->buffer[bufferY * this->scaled.size.x + bufferX] = {color.r, color.g, color.b, static_cast<T>(bitmap[j * w + i])};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OpaqueType::SemiOpaque:
|
||||
case OpaqueType::Transparent: {
|
||||
for (int j = 0; j < h; j++) {
|
||||
for (int i = 0; i < w; i++) {
|
||||
int bufferX = x + i + c_x1 + offsetX;
|
||||
int bufferY = currentY + j + c_y1 + offsetY;
|
||||
|
||||
// Only draw pixels that are within our scaled buffer bounds
|
||||
if constexpr(Scaling) {
|
||||
if (bufferX >= 0 && bufferX < ScalingBase<T, true, Owning, Alignment>::bufferWidth && bufferY >= 0 && bufferY < ScalingBase<T, true, Owning, Alignment>::bufferHeight) {
|
||||
ScalingBase<T, true, Owning, Alignment>::scalingBuffer[bufferY * ScalingBase<T, true, Owning, Alignment>::bufferWidth + bufferX] = {color.r, color.g, color.b, static_cast<T>(bitmap[j * w + i])};
|
||||
}
|
||||
} else {
|
||||
if (bufferX >= 0 && bufferX < (int)this->scaled.size.x && bufferY >= 0 && bufferY < (int)this->scaled.size.y) {
|
||||
if constexpr(std::same_as<T, std::uint8_t>) {
|
||||
std::uint8_t alpha = bitmap[j * w + i];
|
||||
|
||||
Vector<T, 4, Alignment> dst = this->buffer[bufferY * this->scaled.size.x + bufferX];
|
||||
|
||||
float srcA = (alpha / 255.0f) * (color.a / 255.0f);
|
||||
float dstA = dst.a / 255.0f;
|
||||
|
||||
float oneMinusSrcA = 1.0f - color.a;
|
||||
|
||||
float outA = srcA + dstA * (1.0f - srcA);
|
||||
this->buffer[bufferY * this->scaled.size.x + bufferX] = Vector<std::uint8_t, 4, Alignment>(
|
||||
static_cast<std::uint8_t>((color.r * srcA + dst.r * dstA * (1.0f - srcA)) / outA),
|
||||
static_cast<std::uint8_t>((color.g * srcA + dst.g * dstA * (1.0f - srcA)) / outA),
|
||||
static_cast<std::uint8_t>((color.b * srcA + dst.b * dstA * (1.0f - srcA)) / outA),
|
||||
static_cast<std::uint8_t>(outA * 255)
|
||||
);
|
||||
} else if constexpr(std::same_as<T, _Float16>) {
|
||||
std::uint8_t alpha = bitmap[j * w + i];
|
||||
_Float16 srcA = (_Float16(alpha)/_Float16(255.0f))*color.a;
|
||||
Vector<_Float16, 4, Alignment> dst = this->buffer[bufferY * this->scaled.size.x + bufferX];
|
||||
|
||||
_Float16 outA = srcA + dst.a * (1.0f - srcA);
|
||||
this->buffer[bufferY * this->scaled.size.x + bufferX] = Vector<_Float16, 4, Alignment>(
|
||||
(color.r * srcA + dst.r * dst.a * (1.0f - srcA)),
|
||||
(color.g * srcA + dst.g * dst.a * (1.0f - srcA)),
|
||||
(color.b * srcA + dst.b * dst.a * (1.0f - srcA)),
|
||||
outA
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
x += (int)(ax * scale);
|
||||
|
||||
if (p + 1 < end) {
|
||||
int next;
|
||||
x += (int)stbtt_GetGlyphKernAdvance(&font.font, codepoint, utf8_decode(p+1, &next));
|
||||
}
|
||||
}
|
||||
currentY += lineHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,136 +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:RenderingElement2DBase;
|
||||
import Crafter.Asset;
|
||||
import Crafter.Math;
|
||||
import std;
|
||||
import :Transform2D;
|
||||
|
||||
export namespace Crafter {
|
||||
enum class TextAlignment {
|
||||
Left,
|
||||
Center,
|
||||
Right
|
||||
};
|
||||
|
||||
enum class TextVerticalAlignment {
|
||||
Top,
|
||||
Center,
|
||||
Bottom
|
||||
};
|
||||
|
||||
enum class TextOverflowMode {
|
||||
Clip,
|
||||
Wrap
|
||||
};
|
||||
|
||||
enum class TextScaleMode {
|
||||
None,
|
||||
Font,
|
||||
Element,
|
||||
Buffer
|
||||
};
|
||||
|
||||
template<typename T, std::uint8_t Alignment = 0>
|
||||
struct RenderElement2DScalingOwning {
|
||||
std::vector<Vector<T, 4, Alignment>> scalingBuffer;
|
||||
std::uint32_t bufferWidth;
|
||||
std::uint32_t bufferHeight;
|
||||
RenderElement2DScalingOwning() = default;
|
||||
RenderElement2DScalingOwning(std::uint32_t bufferWidth, std::uint32_t bufferHeight) : scalingBuffer(bufferWidth*bufferHeight), bufferWidth(bufferWidth), bufferHeight(bufferHeight) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, std::uint8_t Alignment = 0>
|
||||
struct RenderElement2DScalingNonOwning {
|
||||
Vector<T, 4, Alignment>* scalingBuffer;
|
||||
std::uint32_t bufferWidth;
|
||||
std::uint32_t bufferHeight;
|
||||
RenderElement2DScalingNonOwning() = default;
|
||||
RenderElement2DScalingNonOwning(Vector<T, 4, Alignment>* scalingBuffer, std::uint32_t bufferWidth, std::uint32_t bufferHeight) : scalingBuffer(scalingBuffer), bufferWidth(bufferWidth), bufferHeight(bufferHeight) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
struct RenderElement2DRotating {
|
||||
float rotation;
|
||||
RenderElement2DRotating() = default;
|
||||
RenderElement2DRotating(float rotation) : rotation(rotation) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
struct EmptyScalingBase {};
|
||||
struct EmptyRotatingBase {};
|
||||
|
||||
template<typename T, bool Scaling, bool Owning, std::uint8_t Alignment = 0>
|
||||
using ScalingBase =
|
||||
std::conditional_t<
|
||||
Scaling,
|
||||
std::conditional_t<Owning,
|
||||
RenderElement2DScalingOwning<T, Alignment>,
|
||||
RenderElement2DScalingNonOwning<T, Alignment>>,
|
||||
EmptyScalingBase
|
||||
>;
|
||||
|
||||
template<bool Rotating>
|
||||
using RotatingBase =
|
||||
std::conditional_t<
|
||||
Rotating,
|
||||
RenderElement2DRotating,
|
||||
EmptyRotatingBase
|
||||
>;
|
||||
|
||||
template<typename T, std::uint8_t Frames = 1>
|
||||
struct RenderingElement2DBase : Transform2D {
|
||||
ScaleData2D oldScale[Frames];
|
||||
bool redraw[Frames];
|
||||
std::vector<Vector<T, 4, 4>> buffer;
|
||||
OpaqueType opaque;
|
||||
RenderingElement2DBase(Anchor2D anchor) : Transform2D(anchor) {
|
||||
for(std::uint8_t i = 0; i < Frames; i++) {
|
||||
this->scaled.size.x = 0;
|
||||
}
|
||||
}
|
||||
RenderingElement2DBase(Anchor2D anchor, OpaqueType opaque) : Transform2D(anchor), opaque(opaque) {
|
||||
for(std::uint8_t i = 0; i < Frames; i++) {
|
||||
this->scaled.size.x = 0;
|
||||
}
|
||||
}
|
||||
void Redraw() {
|
||||
for(std::uint8_t i = 0; i < Frames; i++) {
|
||||
redraw[i] = true;
|
||||
}
|
||||
}
|
||||
void CopyNearestNeighbor(Vector<T, 4>* dst, std::uint16_t dstSizeX, std::uint16_t dstScaledSizeX, std::uint16_t dstScaledSizeY, std::uint16_t offsetX, std::uint16_t offsetY) {
|
||||
for (std::uint16_t y = 0; y < dstScaledSizeY; y++) {
|
||||
std::uint16_t srcY = y * scaled.size.y / dstScaledSizeY;
|
||||
std::uint16_t dstY = y + offsetY;
|
||||
for (std::uint16_t x = 0; x < dstScaledSizeX; x++) {
|
||||
std::uint16_t srcX = x * scaled.size.x / dstScaledSizeX;
|
||||
std::uint16_t dstX = x + offsetX;
|
||||
dst[dstY * dstSizeX + dstX] = buffer[srcY * this->scaled.size.x + srcX];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,477 +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 "../lib/stb_truetype.h"
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include <vulkan/vulkan.h>
|
||||
#endif
|
||||
export module Crafter.Graphics:RenderingElement2DVulkan;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
import Crafter.Asset;
|
||||
import std;
|
||||
import :Transform2D;
|
||||
import :VulkanBuffer;
|
||||
import :Types;
|
||||
import :Window;
|
||||
import :DescriptorHeapVulkan;
|
||||
import :Font;
|
||||
|
||||
export namespace Crafter {
|
||||
struct RenderingElement2DVulkanBase : Transform2D {
|
||||
std::uint16_t index;
|
||||
std::uint16_t bufferX;
|
||||
std::uint16_t bufferY;
|
||||
std::array<VulkanBufferBase*, Window::numFrames> buffers;
|
||||
RenderingElement2DVulkanBase(Anchor2D anchor) : Transform2D(anchor) {
|
||||
|
||||
}
|
||||
RenderingElement2DVulkanBase(Anchor2D anchor, std::uint16_t bufferX, std::uint16_t bufferY) : bufferX(bufferX), bufferY(bufferY), Transform2D(anchor) {
|
||||
|
||||
}
|
||||
RenderingElement2DVulkanBase(Anchor2D anchor, std::uint16_t bufferX, std::uint16_t bufferY, std::array<VulkanBufferBase*, Window::numFrames>&& buffers) : bufferX(bufferX), bufferY(bufferY), buffers(std::move(buffers)), Transform2D(anchor) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
template<bool Owning, bool Mapped, bool Single = false>
|
||||
struct RenderingElement2DVulkan : RenderingElement2DVulkanBase {
|
||||
RenderingElement2DVulkan(Anchor2D anchor) : RenderingElement2DVulkanBase(anchor) {
|
||||
|
||||
}
|
||||
RenderingElement2DVulkan(Anchor2D anchor, RendertargetBase& target, Transform2D& parent) requires(Owning) : RenderingElement2DVulkanBase(anchor) {
|
||||
GetScale(target, parent);
|
||||
this->bufferX = this->scaled.size.x;
|
||||
this->bufferY = this->scaled.size.y;
|
||||
if(Single) {
|
||||
buffers[0] = new VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>();
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
for(std::uint8_t i = 1; i < Window::numFrames; i++) {
|
||||
buffers[i] = buffers[0];
|
||||
}
|
||||
} else {
|
||||
for(std::uint8_t i = 0; i < Window::numFrames; i++) {
|
||||
buffers[i] = new VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>();
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[i])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderingElement2DVulkan(Anchor2D anchor, std::uint16_t bufferX, std::uint16_t bufferY) requires(Owning) : RenderingElement2DVulkanBase(anchor, bufferX, bufferY) {
|
||||
if constexpr(Single) {
|
||||
buffers[0] = new VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>();
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
for(std::uint8_t i = 1; i < Window::numFrames; i++) {
|
||||
buffers[i] = buffers[0];
|
||||
}
|
||||
} else {
|
||||
for(std::uint8_t i = 0; i < Window::numFrames; i++) {
|
||||
buffers[i] = new VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>();
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[i])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderingElement2DVulkan(Anchor2D anchor, std::uint16_t bufferX, std::uint16_t bufferY, std::array<VulkanBufferBase*, Window::numFrames>&& buffers) requires(!Owning) : RenderingElement2DVulkanBase(anchor, bufferX, bufferY, std::move(buffers)) {
|
||||
|
||||
}
|
||||
|
||||
RenderingElement2DVulkan(Anchor2D anchor, const std::filesystem::path& assetPath) requires(Owning && Mapped) : RenderingElement2DVulkanBase(anchor) {
|
||||
TextureAssetInfo info = TextureAsset<_Float16>::LoadInfo(assetPath);
|
||||
this->bufferX = info.sizeX;
|
||||
this->bufferY = info.sizeY;
|
||||
if constexpr(Single) {
|
||||
buffers[0] = new VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>();
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
for(std::uint8_t i = 1; i < Window::numFrames; i++) {
|
||||
buffers[i] = buffers[0];
|
||||
}
|
||||
TextureAsset<Vector<_Float16, 4, 4>>::Load(assetPath, static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->value, this->bufferX, this->bufferY);
|
||||
} else {
|
||||
for(std::uint8_t i = 0; i < Window::numFrames; i++) {
|
||||
buffers[i] = new VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>();
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[i])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
}
|
||||
TextureAsset<Vector<_Float16, 4, 4>>::Load(assetPath, static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->value, this->bufferX, this->bufferY);
|
||||
for(std::uint8_t i = 1; i < Window::numFrames; i++) {
|
||||
std::memcpy(static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[i])->value, static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->value, this->bufferX * this->bufferY * sizeof(_Float16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~RenderingElement2DVulkan() {
|
||||
if constexpr(Owning) {
|
||||
if constexpr(Single) {
|
||||
delete static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0]);
|
||||
} else {
|
||||
for(VulkanBufferBase* buffer : buffers) {
|
||||
delete static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderingElement2DVulkan(RenderingElement2DVulkan&) = delete;
|
||||
RenderingElement2DVulkan& operator=(RenderingElement2DVulkan&) = delete;
|
||||
|
||||
void CreateBuffer(std::uint16_t bufferX, std::uint16_t bufferY) requires(Owning) {
|
||||
this->bufferX = this->scaled.size.x;
|
||||
this->bufferY = this->scaled.size.y;
|
||||
if constexpr(Single) {
|
||||
buffers[0] = new VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>();
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
for(std::uint8_t i = 1; i < Window::numFrames; i++) {
|
||||
buffers[i] = buffers[0];
|
||||
}
|
||||
} else {
|
||||
for(std::uint8_t i = 0; i < Window::numFrames; i++) {
|
||||
buffers[i] = new VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>();
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[i])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ResizeBuffer(RendertargetVulkan& window, DescriptorHeapVulkan& descriptorHeap, std::uint16_t bufferOffset, std::uint16_t bufferX, std::uint16_t bufferY) requires(Owning) {
|
||||
if constexpr(Single) {
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
} else {
|
||||
for(VulkanBufferBase* buffer : buffers) {
|
||||
delete static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffer)->Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY);
|
||||
}
|
||||
}
|
||||
this->bufferX = bufferX;
|
||||
this->bufferY = bufferY;
|
||||
for(std::uint8_t frame = 0; frame < Window::numFrames; frame++) {
|
||||
RenderingElement2DVulkanTransformInfo* val = reinterpret_cast<RenderingElement2DVulkanTransformInfo*>(reinterpret_cast<char*>(window.transformBuffer[frame].value) + sizeof(RenderingElement2DVulkanTransformInfo));
|
||||
val[index].bufferX = this->bufferX;
|
||||
val[index].bufferY = this->bufferY;
|
||||
window.transformBuffer[frame].FlushDevice();
|
||||
}
|
||||
|
||||
VkHostAddressRangeEXT ranges[3] = {
|
||||
{
|
||||
.address = descriptorHeap.resourceHeap[0].value + bufferOffset + Device::descriptorHeapProperties.bufferDescriptorSize * index,
|
||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize
|
||||
},
|
||||
{
|
||||
.address = descriptorHeap.resourceHeap[1].value + bufferOffset + Device::descriptorHeapProperties.bufferDescriptorSize * index,
|
||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize
|
||||
},
|
||||
{
|
||||
.address = descriptorHeap.resourceHeap[2].value + bufferOffset + Device::descriptorHeapProperties.bufferDescriptorSize * index,
|
||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize
|
||||
},
|
||||
};
|
||||
|
||||
VkDeviceAddressRangeKHR bufferRanges[3] {
|
||||
{
|
||||
.address = buffers[0]->address,
|
||||
.size = buffers[0]->size
|
||||
},
|
||||
{
|
||||
.address = buffers[1]->address,
|
||||
.size = buffers[1]->size
|
||||
},
|
||||
{
|
||||
.address = buffers[2]->address,
|
||||
.size = buffers[2]->size
|
||||
},
|
||||
};
|
||||
|
||||
VkResourceDescriptorInfoEXT infos[3] = {
|
||||
{
|
||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.data = { .pAddressRange = &bufferRanges[0]}
|
||||
},
|
||||
{
|
||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.data = { .pAddressRange = &bufferRanges[1]}
|
||||
},
|
||||
{
|
||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.data = { .pAddressRange = &bufferRanges[2]}
|
||||
},
|
||||
};
|
||||
|
||||
Device::vkWriteResourceDescriptorsEXT(Device::device, 3, infos, ranges);
|
||||
for(std::uint8_t i = 0; i < Window::numFrames; i++) {
|
||||
descriptorHeap.resourceHeap[i].FlushDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdatePosition(RendertargetBase& window2, Transform2D& parent) override {
|
||||
RendertargetVulkan& window = static_cast<RendertargetVulkan&>(window2);
|
||||
this->ScaleElement(parent);
|
||||
RenderingElement2DVulkanTransformInfo* val = reinterpret_cast<RenderingElement2DVulkanTransformInfo*>(reinterpret_cast<char*>(window.transformBuffer[window.frame].value) + sizeof(RenderingElement2DVulkanTransformInfo));
|
||||
val[index].scaled = this->scaled;
|
||||
for(Transform2D* child : this->children) {
|
||||
child->UpdatePosition(window, *this);
|
||||
}
|
||||
}
|
||||
|
||||
void GetScale(RendertargetBase& window, Transform2D& parent) {
|
||||
this->ScaleElement(parent);
|
||||
for(Transform2D* child : this->children) {
|
||||
child->UpdatePosition(window, *this);
|
||||
}
|
||||
}
|
||||
|
||||
int utf8_decode(const char* s, int* bytes_consumed) {
|
||||
unsigned char c = s[0];
|
||||
if (c < 0x80) {
|
||||
*bytes_consumed = 1;
|
||||
return c;
|
||||
} else if ((c & 0xE0) == 0xC0) {
|
||||
*bytes_consumed = 2;
|
||||
return ((c & 0x1F) << 6) | (s[1] & 0x3F);
|
||||
} else if ((c & 0xF0) == 0xE0) {
|
||||
*bytes_consumed = 3;
|
||||
return ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F);
|
||||
} else if ((c & 0xF8) == 0xF0) {
|
||||
*bytes_consumed = 4;
|
||||
return ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F);
|
||||
}
|
||||
*bytes_consumed = 1;
|
||||
return 0xFFFD; // replacement char
|
||||
}
|
||||
|
||||
void RenderText(std::span<const std::string_view> lines, float size, Vector<_Float16, 4> color, Font& font, TextAlignment alignment = TextAlignment::Left, std::uint32_t offsetX = 0, std::uint32_t offsetY = 0, OpaqueType opaque = OpaqueType::FullyOpaque) requires(Mapped) {
|
||||
float scale = stbtt_ScaleForPixelHeight(&font.font, size);
|
||||
int baseline = (int)(font.ascent * scale);
|
||||
std::uint32_t lineHeight = (font.ascent - font.descent) * scale;
|
||||
std::uint32_t currentY = baseline;
|
||||
for(std::string_view line : lines) {
|
||||
|
||||
std::uint32_t lineWidth = 0;
|
||||
for (const char c : line) {
|
||||
int advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb);
|
||||
lineWidth += (int)(advance * scale);
|
||||
}
|
||||
|
||||
std::uint32_t x = 0;
|
||||
switch (alignment) {
|
||||
case TextAlignment::Left:
|
||||
x = 0;
|
||||
break;
|
||||
case TextAlignment::Center:
|
||||
x = (this->scaled.size.x - lineWidth) / 2;
|
||||
break;
|
||||
case TextAlignment::Right:
|
||||
x = this->scaled.size.x - lineWidth;
|
||||
break;
|
||||
}
|
||||
|
||||
const char* p = line.data();
|
||||
const char* end = p + line.size();
|
||||
|
||||
while (p < end) {
|
||||
int bytes;
|
||||
int codepoint = utf8_decode(p, &bytes);
|
||||
p += bytes;
|
||||
|
||||
int ax;
|
||||
int lsb;
|
||||
stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb);
|
||||
|
||||
int c_x1, c_y1, c_x2, c_y2;
|
||||
stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2);
|
||||
|
||||
int w = c_x2 - c_x1;
|
||||
int h = c_y2 - c_y1;
|
||||
|
||||
std::vector<unsigned char> bitmap(w * h);
|
||||
stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint);
|
||||
|
||||
// Only render characters that fit within the scaled bounds
|
||||
switch(opaque) {
|
||||
case OpaqueType::FullyOpaque: {
|
||||
for (int j = 0; j < h; j++) {
|
||||
for (int i = 0; i < w; i++) {
|
||||
int bufferX = x + i + c_x1 + offsetX;
|
||||
int bufferY = currentY + j + c_y1 + offsetY;
|
||||
|
||||
if (bufferX >= 0 && bufferX < (int)this->bufferX && bufferY >= 0 && bufferY < (int)this->bufferY) {
|
||||
for(std::uint8_t frame = 0; frame < Window::numFrames; frame++) {
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX] = {color.r, color.g, color.b, static_cast<_Float16>(bitmap[j * w + i])};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OpaqueType::SemiOpaque:
|
||||
case OpaqueType::Transparent: {
|
||||
for (int j = 0; j < h; j++) {
|
||||
for (int i = 0; i < w; i++) {
|
||||
int bufferX = x + i + c_x1 + offsetX;
|
||||
int bufferY = currentY + j + c_y1 + offsetY;
|
||||
|
||||
if (bufferX >= 0 && bufferX < (int)this->bufferX && bufferY >= 0 && bufferY < (int)this->bufferY) {
|
||||
std::uint8_t alpha = bitmap[j * w + i];
|
||||
_Float16 srcA = (_Float16(alpha)/_Float16(255.0f))*color.a;
|
||||
for(std::uint8_t frame = 0; frame < Window::numFrames; frame++) {
|
||||
Vector<_Float16, 4, 4> dst = static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX];
|
||||
|
||||
_Float16 outA = srcA + dst.a * (1.0f - srcA);
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX] = Vector<_Float16, 4, 4>(
|
||||
(color.r * srcA + dst.r * dst.a * (1.0f - srcA)),
|
||||
(color.g * srcA + dst.g * dst.a * (1.0f - srcA)),
|
||||
(color.b * srcA + dst.b * dst.a * (1.0f - srcA)),
|
||||
outA
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
x += (int)(ax * scale);
|
||||
|
||||
if (p + 1 < end) {
|
||||
int next;
|
||||
x += (int)stbtt_GetGlyphKernAdvance(&font.font, codepoint, utf8_decode(p+1, &next));
|
||||
}
|
||||
}
|
||||
currentY += lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderText(std::span<const std::string_view> lines, float size, Vector<_Float16, 4> color, Font& font, std::uint8_t frame, TextAlignment alignment = TextAlignment::Left, TextVerticalAlignment verticalAlignment = TextVerticalAlignment::Top, std::int32_t offsetX = 0, std::int32_t offsetY = 0, OpaqueType opaque = OpaqueType::FullyOpaque) requires(Mapped) {
|
||||
float scale = stbtt_ScaleForPixelHeight(&font.font, size);
|
||||
int baseline = (int)(font.ascent * scale);
|
||||
std::uint32_t lineHeight = (font.ascent - font.descent) * scale;
|
||||
std::uint32_t currentY = baseline;
|
||||
|
||||
std::uint32_t ogOffsetX = offsetX;
|
||||
std::uint32_t ogOffsetY = offsetY;
|
||||
|
||||
for(std::string_view line : lines) {
|
||||
offsetX = ogOffsetX;
|
||||
offsetY = ogOffsetY;
|
||||
|
||||
std::int32_t lineWidth = 0;
|
||||
for (const char c : line) {
|
||||
int advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb);
|
||||
lineWidth += (int)(advance * scale);
|
||||
}
|
||||
|
||||
switch (alignment) {
|
||||
case TextAlignment::Left:
|
||||
break;
|
||||
case TextAlignment::Center:
|
||||
offsetX -= lineWidth / 2;
|
||||
break;
|
||||
case TextAlignment::Right:
|
||||
offsetX -= lineWidth;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (verticalAlignment) {
|
||||
case TextVerticalAlignment::Top:
|
||||
break;
|
||||
case TextVerticalAlignment::Center:
|
||||
offsetY += (lineHeight / 2) - (size);
|
||||
break;
|
||||
case TextVerticalAlignment::Bottom:
|
||||
offsetY += lineHeight;
|
||||
break;
|
||||
}
|
||||
|
||||
const char* p = line.data();
|
||||
const char* end = p + line.size();
|
||||
|
||||
while (p < end) {
|
||||
int bytes;
|
||||
int codepoint = utf8_decode(p, &bytes);
|
||||
p += bytes;
|
||||
|
||||
int ax;
|
||||
int lsb;
|
||||
stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb);
|
||||
|
||||
int c_x1, c_y1, c_x2, c_y2;
|
||||
stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2);
|
||||
|
||||
int w = c_x2 - c_x1;
|
||||
int h = c_y2 - c_y1;
|
||||
|
||||
std::vector<unsigned char> bitmap(w * h);
|
||||
stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint);
|
||||
|
||||
// Only render characters that fit within the scaled bounds
|
||||
switch(opaque) {
|
||||
case OpaqueType::FullyOpaque: {
|
||||
for (int j = 0; j < h; j++) {
|
||||
for (int i = 0; i < w; i++) {
|
||||
int bufferX = offsetX + i + c_x1;
|
||||
int bufferY = currentY + j + c_y1 + offsetY;
|
||||
|
||||
if (bufferX >= 0 && bufferX < (int)this->bufferX && bufferY >= 0 && bufferY < (int)this->bufferY) {
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX] = {color.r, color.g, color.b, static_cast<_Float16>(bitmap[j * w + i])};
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OpaqueType::SemiOpaque:
|
||||
case OpaqueType::Transparent: {
|
||||
for (int j = 0; j < h; j++) {
|
||||
for (int i = 0; i < w; i++) {
|
||||
int bufferX = offsetX + i + c_x1;
|
||||
int bufferY = currentY + j + c_y1 + offsetY;
|
||||
|
||||
if (bufferX >= 0 && bufferX < (int)this->bufferX && bufferY >= 0 && bufferY < (int)this->bufferY) {
|
||||
std::uint8_t alpha = bitmap[j * w + i];
|
||||
_Float16 srcA = (_Float16(alpha)/_Float16(255.0f))*color.a;
|
||||
Vector<_Float16, 4, 4> dst = static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX];
|
||||
|
||||
_Float16 outA = srcA + dst.a * (1.0f - srcA);
|
||||
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX] = Vector<_Float16, 4, 4>(
|
||||
(color.r * srcA + dst.r * dst.a * (1.0f - srcA)),
|
||||
(color.g * srcA + dst.g * dst.a * (1.0f - srcA)),
|
||||
(color.b * srcA + dst.b * dst.a * (1.0f - srcA)),
|
||||
outA
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
offsetX += (int)(ax * scale);
|
||||
|
||||
if (p + 1 < end) {
|
||||
int next;
|
||||
offsetX += (int)stbtt_GetGlyphKernAdvance(&font.font, codepoint, utf8_decode(p+1, &next));
|
||||
}
|
||||
}
|
||||
currentY += lineHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
|
@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
*/
|
||||
|
||||
module;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
export module Crafter.Graphics:RenderingElement3D;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
import std;
|
||||
import :Mesh;
|
||||
import :VulkanBuffer;
|
||||
|
|
@ -45,5 +42,4 @@ export namespace Crafter {
|
|||
inline static TlasWithBuffer tlases[Window::numFrames];
|
||||
static void BuildTLAS(VkCommandBuffer cmd, std::uint32_t index);
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
@ -1,326 +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 "../lib/stb_truetype.h"
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include <vulkan/vulkan.h>
|
||||
#endif
|
||||
export module Crafter.Graphics:Rendertarget;
|
||||
import Crafter.Math;
|
||||
import Crafter.Asset;
|
||||
import std;
|
||||
import :Types;
|
||||
import :Transform2D;
|
||||
import :RenderingElement2DBase;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
import :Device;
|
||||
import :VulkanBuffer;
|
||||
#endif
|
||||
|
||||
export namespace Crafter {
|
||||
struct RendertargetBase {
|
||||
#ifdef CRAFTER_TIMING
|
||||
std::vector<std::tuple<const Transform*, std::uint32_t, std::uint32_t, std::chrono::nanoseconds>> renderTimings;
|
||||
#endif
|
||||
Transform2D transform;
|
||||
std::uint16_t sizeX;
|
||||
std::uint16_t sizeY;
|
||||
RendertargetBase() = default;
|
||||
RendertargetBase(std::uint16_t sizeX, std::uint16_t sizeY) : sizeX(sizeX), sizeY(sizeY), transform({0, 0, 1, 1, 0, 0, 0}){
|
||||
transform.scaled.size.x = sizeX;
|
||||
transform.scaled.size.y = sizeY;
|
||||
transform.scaled.position.x = 0;
|
||||
transform.scaled.position.y = 0;
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
struct RenderingElement2DVulkanBase;
|
||||
|
||||
struct __attribute__((packed)) RenderingElement2DVulkanTransformInfo {
|
||||
ScaleData2D scaled; // 0 - 8 bytes
|
||||
std::uint16_t bufferX; // 8 - 2 bytes
|
||||
std::uint16_t bufferY; // 10 - 2 bytes
|
||||
//12 bytes total;
|
||||
};
|
||||
|
||||
|
||||
struct DescriptorHeapVulkan;
|
||||
struct RendertargetVulkan : RendertargetBase {
|
||||
std::uint8_t frame;
|
||||
std::vector<RenderingElement2DVulkanBase*> elements;
|
||||
VulkanBuffer<RenderingElement2DVulkanTransformInfo, true> transformBuffer[3];
|
||||
|
||||
RendertargetVulkan() = default;
|
||||
RendertargetVulkan(std::uint16_t sizeX, std::uint16_t sizeY);
|
||||
void UpdateElements();
|
||||
void CreateBuffer(std::uint8_t frame);
|
||||
void ReorderBuffer(std::uint8_t frame);
|
||||
void WriteDescriptors(std::span<VkResourceDescriptorInfoEXT> infos, std::span<VkHostAddressRangeEXT> ranges, std::uint16_t start, std::uint32_t bufferOffset, DescriptorHeapVulkan& descriptorHeap);
|
||||
void SetOrderResursive(Transform2D* elementTransform);
|
||||
};
|
||||
#endif
|
||||
|
||||
template<typename T, std::uint8_t Channels, std::uint8_t Alignment, std::uint8_t Frames>
|
||||
struct Rendertarget : RendertargetBase {
|
||||
Vector<T, Channels, Alignment>* buffer[Frames];
|
||||
Rendertarget() = default;
|
||||
Rendertarget(std::uint16_t sizeX, std::uint16_t sizeY) : RendertargetBase(sizeX, sizeY) {
|
||||
|
||||
}
|
||||
void RenderElement(Transform2D* elementTransform, std::uint8_t frame, std::vector<ClipRect>&& dirtyRects) {
|
||||
RenderingElement2DBase<T, Frames>* element = dynamic_cast<RenderingElement2DBase<T, Frames>*>(elementTransform);
|
||||
if(element) {
|
||||
#ifdef CRAFTER_TIMING
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
#endif
|
||||
|
||||
if(element->scaled.size.x < 1 || element->scaled.size.y < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(ClipRect dirty : dirtyRects) {
|
||||
dirty.left = std::uint16_t(std::max(element->scaled.position.x, std::int16_t(dirty.left)));
|
||||
dirty.top = std::uint16_t(std::max(element->scaled.position.y,std::int16_t(dirty.top)));
|
||||
dirty.right = std::min(std::uint16_t(element->scaled.position.x+element->scaled.size.x), dirty.right);
|
||||
dirty.bottom = std::min(std::uint16_t(element->scaled.position.y+element->scaled.size.y), dirty.bottom);
|
||||
|
||||
if(dirty.right <= dirty.left || dirty.bottom <= dirty.top) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Vector<T, 4, 4>* src_buffer = element->buffer.data();
|
||||
std::uint16_t src_width = element->scaled.size.x;
|
||||
std::uint16_t src_height = element->scaled.size.y;
|
||||
|
||||
switch (element->opaque) {
|
||||
case OpaqueType::FullyOpaque: {
|
||||
for (std::uint16_t y = dirty.top; y < dirty.bottom; y++) {
|
||||
std::uint16_t src_y = y - element->scaled.position.y;
|
||||
std::uint16_t src_x = dirty.left - element->scaled.position.x;
|
||||
std::memcpy(&this->buffer[frame][y * this->sizeX + dirty.left], &src_buffer[src_y * src_width + src_x], (dirty.right - dirty.left) * sizeof(Vector<T, Channels, Alignment>));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OpaqueType::SemiOpaque:
|
||||
case OpaqueType::Transparent:
|
||||
if constexpr(std::same_as<T, _Float16>) {
|
||||
for (std::uint16_t y = dirty.top; y < dirty.bottom; y++) {
|
||||
std::uint16_t src_y = y - element->scaled.position.y;
|
||||
std::uint16_t pixel_width = dirty.right - dirty.left;
|
||||
|
||||
constexpr std::uint32_t simd_width = VectorF16<1, 1>::MaxElement / 4;
|
||||
std::uint32_t rows = pixel_width / simd_width;
|
||||
|
||||
for (std::uint32_t x = 0; x < rows; x++) {
|
||||
std::uint16_t px = dirty.left + x * simd_width;
|
||||
std::uint16_t src_x = px - element->scaled.position.x;
|
||||
|
||||
VectorF16<4, simd_width> src(&src_buffer[src_y * src_width + src_x].v[0]);
|
||||
VectorF16<4, simd_width> dst(&buffer[frame][y * this->sizeX + px].v[0]);
|
||||
VectorF16<4, simd_width> oneMinusSrcA = VectorF16<4, simd_width>(1) - src.Shuffle<{{3, 3, 3, 3}}>();
|
||||
VectorF16<4, simd_width> result = VectorF16<4, simd_width>::MulitplyAdd(dst, oneMinusSrcA, src);
|
||||
result.Store(&buffer[frame][y * this->sizeX + px].v[0]);
|
||||
}
|
||||
|
||||
std::uint32_t remainder = pixel_width - (rows * simd_width);
|
||||
std::uint16_t remainder_start = dirty.left + rows * simd_width;
|
||||
|
||||
for (std::uint8_t x = 0; x < remainder; x++) {
|
||||
std::uint16_t px = remainder_start + x;
|
||||
std::uint16_t src_x = px - element->scaled.position.x;
|
||||
|
||||
Vector<T, Channels, Alignment> src = src_buffer[src_y * src_width + src_x];
|
||||
Vector<T, Channels, Alignment> dst = buffer[frame][y * this->sizeX + px];
|
||||
_Float16 oneMinusSrcA = (_Float16)1.0f - src.a;
|
||||
|
||||
buffer[frame][y * this->sizeX + px] = Vector<T, Channels, Alignment>(
|
||||
src.r + dst.r * oneMinusSrcA,
|
||||
src.g + dst.g * oneMinusSrcA,
|
||||
src.b + dst.b * oneMinusSrcA,
|
||||
src.a + dst.a * oneMinusSrcA
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (std::uint16_t y = dirty.top; y < dirty.bottom; y++) {
|
||||
std::uint16_t src_y = y - element->scaled.position.y;
|
||||
std::uint16_t src_x = dirty.left - element->scaled.position.x;
|
||||
std::memcpy(&this->buffer[frame][y * this->sizeX + dirty.left], &src_buffer[src_y * src_width + src_x], (dirty.right - dirty.left) * sizeof(Vector<T, Channels, Alignment>));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
#ifdef CRAFTER_TIMING
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
renderTimings.push_back({element, element->scaled.size.x, element->scaled.size.y, end-start});
|
||||
#endif
|
||||
}
|
||||
std::sort(elementTransform->children.begin(), elementTransform->children.end(), [](Transform2D* a, Transform2D* b){ return a->anchor.z < b->anchor.z; });
|
||||
for(Transform2D* child : elementTransform->children) {
|
||||
this->RenderElement(child, frame, std::move(dirtyRects));
|
||||
}
|
||||
}
|
||||
|
||||
void AddOldRects(Transform2D* elementTransform, std::uint8_t frame, std::vector<ClipRect>& clipRects) {
|
||||
RenderingElement2DBase<T, Frames>* element = dynamic_cast<RenderingElement2DBase<T, Frames>*>(elementTransform);
|
||||
if(element) {
|
||||
if(element->scaled.position.x != element->oldScale[frame].position.x || element->scaled.position.y != element->oldScale[frame].position.y || element->scaled.size.x != element->oldScale[frame].size.x || element->scaled.size.y != element->oldScale[frame].size.y || element->redraw[frame]) {
|
||||
clipRects.emplace_back(std::max(element->scaled.position.x, std::int16_t(0)), std::min(std::int16_t(element->scaled.position.x + element->scaled.size.x), std::int16_t(this->sizeX)), std::max(element->scaled.position.y, std::int16_t(0)), std::min(std::int16_t(element->scaled.position.y + element->scaled.size.y), std::int16_t(this->sizeY)));
|
||||
clipRects.emplace_back(std::max(element->oldScale[frame].position.x, std::int16_t(0)), std::min(std::int16_t(element->oldScale[frame].position.x + element->oldScale[frame].size.x), std::int16_t(this->sizeX)), std::max(element->oldScale[frame].position.y, std::int16_t(0)), std::min(std::int16_t(element->oldScale[frame].position.y + element->oldScale[frame].size.y), std::int16_t(this->sizeY)));
|
||||
element->oldScale[frame] = element->scaled;
|
||||
element->redraw[frame] = false;
|
||||
} else if(element->redraw[frame]) {
|
||||
clipRects.emplace_back(std::max(element->scaled.position.x, std::int16_t(0)), std::min(std::int16_t(element->scaled.position.x + element->scaled.size.x), std::int16_t(this->sizeX)), std::max(element->scaled.position.y, std::int16_t(0)), std::min(std::int16_t(element->scaled.position.y + element->scaled.size.y), std::int16_t(this->sizeY)));
|
||||
element->oldScale[frame] = element->scaled;
|
||||
element->redraw[frame] = false;
|
||||
}
|
||||
}
|
||||
for(Transform2D* child : elementTransform->children) {
|
||||
AddOldRects(child, frame, clipRects);
|
||||
}
|
||||
}
|
||||
|
||||
bool Render(std::uint8_t frame) {
|
||||
std::sort(this->transform.children.begin(), this->transform.children.end(), [](Transform2D* a, Transform2D* b){ return a->anchor.z < b->anchor.z; });
|
||||
std::vector<ClipRect> clipRects;
|
||||
for(Transform2D* child : this->transform.children) {
|
||||
AddOldRects(child, frame, clipRects);
|
||||
}
|
||||
|
||||
//std::vector<ClipRect> newClip;
|
||||
// for (std::uint32_t i = 0; i < dirtyRects.size(); i++) {
|
||||
// ClipRect rect = dirtyRects[i];
|
||||
// for (std::uint32_t i2 = i + 1; i2 < dirtyRects.size(); i2++) {
|
||||
// ClipRect existing = dirtyRects[i2];
|
||||
// if(rect.bottom >= existing.top && rect.top <= existing.top) {
|
||||
// newClip.push_back({
|
||||
// .left = rect.left,
|
||||
// .right = rect.right,
|
||||
// .top = rect.top,
|
||||
// .bottom = existing.top,
|
||||
// });
|
||||
// //-| shape
|
||||
// if(rect.right > existing.right) {
|
||||
// newClip.push_back({
|
||||
// .left = existing.right,
|
||||
// .right = rect.right,
|
||||
// .top = existing.top,
|
||||
// .bottom = existing.bottom,
|
||||
// });
|
||||
// }
|
||||
// //|- shape
|
||||
// if(rect.left < existing.left) {
|
||||
// newClip.push_back({
|
||||
// .left = rect.left,
|
||||
// .right = existing.left,
|
||||
// .top = existing.top,
|
||||
// .bottom = existing.bottom,
|
||||
// });
|
||||
// }
|
||||
// //-| or |- shape where rect extends further down
|
||||
// if(rect.bottom > existing.bottom) {
|
||||
// newClip.push_back({
|
||||
// .left = rect.left,
|
||||
// .right = rect.right,
|
||||
// .top = existing.bottom,
|
||||
// .bottom = rect.bottom,
|
||||
// });
|
||||
// }
|
||||
// goto inner;
|
||||
// }
|
||||
// if (rect.left <= existing.right && rect.right >= existing.left) {
|
||||
// newClip.push_back({
|
||||
// .left = rect.left,
|
||||
// .right = existing.left,
|
||||
// .top = rect.top,
|
||||
// .bottom = rect.bottom,
|
||||
// });
|
||||
// if (rect.right > existing.right) {
|
||||
// newClip.push_back({
|
||||
// .left = existing.right,
|
||||
// .right = rect.right,
|
||||
// .top = rect.top,
|
||||
// .bottom = rect.bottom,
|
||||
// });
|
||||
// }
|
||||
// goto inner;
|
||||
// }
|
||||
// }
|
||||
// newClip.push_back(rect);
|
||||
// inner:;
|
||||
// }
|
||||
|
||||
//dirtyRects = std::move(newClip);
|
||||
|
||||
// std::memset(buffer, 0, width*height*4);
|
||||
|
||||
// std::cout << dirtyRects.size() << std::endl;
|
||||
// // Color palette
|
||||
// static const std::vector<Vector<std::uint8_t, 4>> colors = {
|
||||
// {255, 0, 0, 255}, // red
|
||||
// { 0, 255, 0, 255}, // green
|
||||
// { 0, 0, 255, 255}, // blue
|
||||
// {255, 255, 0, 255}, // yellow
|
||||
// {255, 0, 255, 255}, // magenta
|
||||
// { 0, 255, 255, 255}, // cyan
|
||||
// };
|
||||
|
||||
// std::size_t rectIndex = 0;
|
||||
|
||||
// for (const ClipRect& rect : dirtyRects) {
|
||||
// const Vector<std::uint8_t, 4>& color = colors[rectIndex % colors.size()];
|
||||
|
||||
// std::cout << std::format(
|
||||
// "ClipRect {}: [{}, {}, {}, {}] Color = RGBA({}, {}, {}, {})",
|
||||
// rectIndex,
|
||||
// rect.left, rect.top, rect.right, rect.bottom,
|
||||
// color.r, color.g, color.b, color.a
|
||||
// ) << std::endl;
|
||||
|
||||
// for (std::int32_t y = rect.top; y < rect.bottom; ++y) {
|
||||
// for (std::int32_t x = rect.left; x < rect.right; ++x) {
|
||||
// buffer[y * width + x] = color;
|
||||
// }
|
||||
// }
|
||||
|
||||
// ++rectIndex;
|
||||
// }
|
||||
|
||||
if (!clipRects.empty()) {
|
||||
for (ClipRect rect : clipRects) {
|
||||
for (std::int32_t y = rect.top; y < rect.bottom; y++) {
|
||||
for (std::int32_t x = rect.left; x < rect.right; x++) {
|
||||
this->buffer[frame][y * this->sizeX + x] = {0, 0, 0, 0};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Transform2D* child : this->transform.children) {
|
||||
RenderElement(child, frame, std::move(clipRects));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -19,9 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
|
||||
module;
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
|
||||
export module Crafter.Graphics:SamplerVulkan;
|
||||
import std;
|
||||
|
|
@ -29,7 +27,6 @@ import :VulkanBuffer;
|
|||
import :ImageVulkan;
|
||||
|
||||
export namespace Crafter {
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
template <typename PixelType>
|
||||
class SamplerVulkan {
|
||||
public:
|
||||
|
|
@ -60,5 +57,4 @@ export namespace Crafter {
|
|||
};
|
||||
}
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
|
@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
*/
|
||||
|
||||
module;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
export module Crafter.Graphics:ShaderBindingTableVulkan;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
import std;
|
||||
import :Device;
|
||||
import :VulkanBuffer;
|
||||
|
|
@ -40,6 +37,4 @@ export namespace Crafter {
|
|||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
|
@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
*/
|
||||
|
||||
module;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
export module Crafter.Graphics:ShaderVulkan;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
import std;
|
||||
import :Device;
|
||||
import :Types;
|
||||
|
|
@ -62,6 +59,4 @@ export namespace Crafter {
|
|||
Device::CheckVkResult(vkCreateShaderModule(Device::device, &module_info, nullptr, &shader));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
|
@ -1,77 +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:Transform2D;
|
||||
import std;
|
||||
import :Types;
|
||||
import :ForwardDeclarations;
|
||||
|
||||
export namespace Crafter {
|
||||
struct Anchor2D {
|
||||
float x;
|
||||
float y;
|
||||
float width;
|
||||
float height;
|
||||
float offsetX;
|
||||
float offsetY;
|
||||
std::uint8_t z;
|
||||
bool maintainAspectRatio;
|
||||
Anchor2D() = default;
|
||||
Anchor2D(float x, float y, float width, float height, float offsetX, float offsetY, std::uint8_t z, bool maintainAspectRatio = false);
|
||||
};
|
||||
|
||||
struct Transform2D {
|
||||
Anchor2D anchor;
|
||||
ScaleData2D scaled;
|
||||
std::vector<Transform2D*> children;
|
||||
Transform2D() = default;
|
||||
Transform2D(Anchor2D anchor) : anchor(anchor) {
|
||||
|
||||
}
|
||||
Transform2D(Transform2D&) = delete;
|
||||
Transform2D(Transform2D&&) = delete;
|
||||
Transform2D& operator=(Transform2D&) = delete;
|
||||
virtual ~Transform2D() = default;
|
||||
|
||||
virtual void UpdatePosition(RendertargetBase& window, Transform2D& parent) {
|
||||
ScaleElement(parent);
|
||||
for(Transform2D* child : children) {
|
||||
child->UpdatePosition(window, *this);
|
||||
}
|
||||
}
|
||||
|
||||
void ScaleElement(Transform2D& parent) {
|
||||
if(anchor.maintainAspectRatio) {
|
||||
if(parent.scaled.size.x > parent.scaled.size.y) {
|
||||
scaled.size.x = anchor.width * parent.scaled.size.y;
|
||||
scaled.size.y = anchor.height * parent.scaled.size.y;
|
||||
} else {
|
||||
scaled.size.x = anchor.width * parent.scaled.size.x;
|
||||
scaled.size.y = anchor.height * parent.scaled.size.x;
|
||||
}
|
||||
} else {
|
||||
scaled.size.x = anchor.width * parent.scaled.size.x;
|
||||
scaled.size.y = anchor.height * parent.scaled.size.y;
|
||||
}
|
||||
|
||||
scaled.position.x = parent.scaled.position.x + (anchor.x * parent.scaled.size.x - anchor.offsetX * scaled.size.x);
|
||||
scaled.position.y = parent.scaled.position.y + (anchor.y * parent.scaled.size.y - anchor.offsetY * scaled.size.y);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -18,9 +18,7 @@ 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;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
export module Crafter.Graphics:Types;
|
||||
import std;
|
||||
import Crafter.Math;
|
||||
|
|
@ -247,10 +245,8 @@ export namespace Crafter {
|
|||
return std::tan(fov * std::numbers::pi / 360.0);
|
||||
}
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
struct DescriptorBinding {
|
||||
VkDescriptorType type;
|
||||
std::uint32_t slot;
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
30
interfaces/Crafter.Graphics-UI.cppm
Normal file
30
interfaces/Crafter.Graphics-UI.cppm
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
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:UI;
|
||||
|
||||
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;
|
||||
100
interfaces/Crafter.Graphics-UIAtlas.cppm
Normal file
100
interfaces/Crafter.Graphics-UIAtlas.cppm
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
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:UIAtlas;
|
||||
import std;
|
||||
import :Font;
|
||||
import :ImageVulkan;
|
||||
import :Device;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
// 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.
|
||||
struct Glyph {
|
||||
float u0 = 0, v0 = 0; // top-left UV in the atlas
|
||||
float u1 = 0, v1 = 0; // bottom-right UV in the atlas
|
||||
float w = 0, h = 0; // glyph quad size in atlas px (= the bitmap size)
|
||||
float xoff = 0, yoff = 0; // glyph bearing relative to baseline cursor
|
||||
float advance = 0; // horizontal advance at base size, in atlas px
|
||||
};
|
||||
|
||||
// Single-channel SDF atlas. Glyphs are rasterised with stb_truetype's
|
||||
// GetGlyphSDF at a fixed `kBaseSize` resolution and packed via a simple
|
||||
// shelf allocator. Drawing scales the glyph quad linearly; the shader
|
||||
// resolves edge AA via screen-space derivatives, so a single atlas
|
||||
// serves all sizes and DPI scales without re-bake.
|
||||
class FontAtlas {
|
||||
public:
|
||||
// Build-time constants. Tweak in one place if needed; values picked
|
||||
// to give crisp text from ~10pt to ~96pt and leave headroom in the
|
||||
// SDF distance band so smoothstep is in the linear regime.
|
||||
static constexpr int kAtlasSize = 1024;
|
||||
static constexpr float kBaseSize = 32.0f; // pixel-height at which we rasterise
|
||||
static constexpr int kPadding = 4; // distance-field padding around each glyph
|
||||
static constexpr int kOnEdgeValue = 128; // 8-bit value mapped to "0 distance"
|
||||
static constexpr float kPixelDistScale = 32.0f; // how many distance units per pixel — wider = softer AA range
|
||||
|
||||
ImageVulkan<std::uint8_t> image;
|
||||
bool dirty = false; // staging has unflushed writes
|
||||
|
||||
// Allocate the GPU image and zero-clear it. Must be called once
|
||||
// with a one-shot init command buffer.
|
||||
void Initialize(VkCommandBuffer cmd);
|
||||
|
||||
// Rasterise + pack the glyph if it isn't cached yet. Returns
|
||||
// false only if the atlas is out of space (V2: grow). After a
|
||||
// successful Ensure the bitmap lives in `image.buffer.value` and
|
||||
// `dirty` is true; call Update(cmd) before reading on the GPU.
|
||||
bool Ensure(Font& font, std::uint32_t codepoint);
|
||||
|
||||
// Lookup is cheap (hash-table). Returns nullptr if the glyph
|
||||
// hasn't been Ensured.
|
||||
const Glyph* Lookup(Font& font, std::uint32_t codepoint) const;
|
||||
|
||||
// If `dirty`, flushes staging into the GPU image and transitions
|
||||
// it back to SHADER_READ_ONLY_OPTIMAL. No-op if not dirty.
|
||||
void Update(VkCommandBuffer cmd);
|
||||
|
||||
private:
|
||||
// Shelf packer state.
|
||||
struct Shelf { int y = 0; int height = 0; int cursorX = 0; };
|
||||
std::vector<Shelf> shelves_;
|
||||
int nextShelfY_ = 0;
|
||||
|
||||
// (font*, codepoint) → Glyph cache.
|
||||
struct Key {
|
||||
const Font* font;
|
||||
std::uint32_t cp;
|
||||
bool operator==(const Key&) const = default;
|
||||
};
|
||||
struct KeyHash {
|
||||
std::size_t operator()(const Key& k) const noexcept {
|
||||
std::size_t h1 = std::hash<const void*>{}(k.font);
|
||||
std::size_t h2 = std::hash<std::uint32_t>{}(k.cp);
|
||||
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
|
||||
}
|
||||
};
|
||||
std::unordered_map<Key, Glyph, KeyHash> cache_;
|
||||
|
||||
// Place a wxh glyph; returns true + writes top-left into outX/outY.
|
||||
bool ShelfPlace(int w, int h, int& outX, int& outY);
|
||||
};
|
||||
}
|
||||
167
interfaces/Crafter.Graphics-UIDrawList.cppm
Normal file
167
interfaces/Crafter.Graphics-UIDrawList.cppm
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
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)
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
50
interfaces/Crafter.Graphics-UIHit.cppm
Normal file
50
interfaces/Crafter.Graphics-UIHit.cppm
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
interfaces/Crafter.Graphics-UILayout.cppm
Normal file
73
interfaces/Crafter.Graphics-UILayout.cppm
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
98
interfaces/Crafter.Graphics-UILength.cppm
Normal file
98
interfaces/Crafter.Graphics-UILength.cppm
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
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};
|
||||
}
|
||||
};
|
||||
}
|
||||
110
interfaces/Crafter.Graphics-UIRenderer.cppm
Normal file
110
interfaces/Crafter.Graphics-UIRenderer.cppm
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
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();
|
||||
};
|
||||
}
|
||||
111
interfaces/Crafter.Graphics-UIScene.cppm
Normal file
111
interfaces/Crafter.Graphics-UIScene.cppm
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
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 WindowScale() const;
|
||||
void RebuildFrame();
|
||||
};
|
||||
}
|
||||
80
interfaces/Crafter.Graphics-UITheme.cppm
Normal file
80
interfaces/Crafter.Graphics-UITheme.cppm
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
189
interfaces/Crafter.Graphics-UIWidget.cppm
Normal file
189
interfaces/Crafter.Graphics-UIWidget.cppm
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
989
interfaces/Crafter.Graphics-UIWidgets.cppm
Normal file
989
interfaces/Crafter.Graphics-UIWidgets.cppm
Normal file
|
|
@ -0,0 +1,989 @@
|
|||
/*
|
||||
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:UIWidgets;
|
||||
import std;
|
||||
import :UILength;
|
||||
import :UIWidget;
|
||||
import :UILayout;
|
||||
import :UIDrawList;
|
||||
import :UIAtlas;
|
||||
import :Font;
|
||||
|
||||
export namespace Crafter::UI {
|
||||
// ─────────────────── Text-emission helper ─────────────────────────────
|
||||
// Walks an ASCII string, ensuring each codepoint in the atlas and
|
||||
// emitting one Glyph item per non-whitespace glyph. Returns the final
|
||||
// x-cursor position (useful when emitting multi-run text).
|
||||
namespace detail {
|
||||
inline float EmitText(DrawList& dl, Font* font, std::string_view text,
|
||||
float fontSizePx, Color color,
|
||||
float originX, float topY) {
|
||||
if (!dl.atlas || !font) return originX;
|
||||
FontAtlas& atlas = *dl.atlas;
|
||||
|
||||
float scaleFactor = fontSizePx / FontAtlas::kBaseSize;
|
||||
float baselineY = topY + font->AscentPx(fontSizePx);
|
||||
float cursorX = originX;
|
||||
|
||||
std::size_t i = 0;
|
||||
while (i < text.size()) {
|
||||
std::uint32_t cp = DecodeUtf8(text, i);
|
||||
if (cp == 0) break;
|
||||
atlas.Ensure(*font, cp);
|
||||
const Glyph* g = atlas.Lookup(*font, cp);
|
||||
if (!g) continue;
|
||||
if (g->w > 0 && g->h > 0) {
|
||||
Rect quad{
|
||||
cursorX + g->xoff * scaleFactor,
|
||||
baselineY + g->yoff * scaleFactor,
|
||||
g->w * scaleFactor,
|
||||
g->h * scaleFactor,
|
||||
};
|
||||
dl.AddGlyph(quad, color, {g->u0, g->v0, g->u1 - g->u0, g->v1 - g->v0});
|
||||
}
|
||||
cursorX += g->advance * scaleFactor;
|
||||
}
|
||||
return cursorX;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── Spacer ───────────────────────────────────
|
||||
// Takes up flex space along whichever axis its parent stacks on. Has
|
||||
// no minimum size of its own; an HStack treats it as a horizontal gap,
|
||||
// a VStack as vertical.
|
||||
struct Spacer : WidgetBuilder<Spacer> {
|
||||
Spacer() {
|
||||
width_ = Length::Frac(1);
|
||||
height_ = Length::Frac(1);
|
||||
}
|
||||
|
||||
Size Measure(Size /*avail*/, const LayoutContext& /*ctx*/) override {
|
||||
desiredSize = {0, 0};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& /*ctx*/) override {
|
||||
computedRect = rect;
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────── Stack-axis helpers ───────────────────────────
|
||||
namespace detail {
|
||||
// Pick a child's cross-axis size (the axis the stack does NOT lay
|
||||
// children along). For Auto, defer to the child's measured size;
|
||||
// for Px/Pct/Frac, resolve against the available content size.
|
||||
inline float ResolveCrossAxis(const Widget& c, Length len, float contentExtent,
|
||||
float scale, float autoSize) {
|
||||
return ResolveLength(len, contentExtent, scale, [&]{ return autoSize; });
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── VStack ───────────────────────────────────
|
||||
struct VStack : WidgetBuilder<VStack> {
|
||||
float spacing_ = 0;
|
||||
|
||||
VStack& spacing(float s) { spacing_ = s; return *this; }
|
||||
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
float spacingPx = spacing_ * ctx.scale;
|
||||
|
||||
// If our own width/height is fixed, that bounds children; if Auto,
|
||||
// children may use the full available extent.
|
||||
float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; });
|
||||
float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; });
|
||||
float contentW = std::max(0.0f, ownW - p.Horiz());
|
||||
float contentH = std::max(0.0f, ownH - p.Vert());
|
||||
|
||||
float maxChildW = 0;
|
||||
float totalH = 0;
|
||||
float remainingH = contentH;
|
||||
for (std::size_t i = 0; i < children_.size(); ++i) {
|
||||
auto& c = *children_[i];
|
||||
Size childAvail = { contentW, std::max(0.0f, remainingH) };
|
||||
Size cs = c.Measure(childAvail, ctx);
|
||||
maxChildW = std::max(maxChildW, cs.w);
|
||||
totalH += cs.h;
|
||||
remainingH -= cs.h;
|
||||
if (i + 1 < children_.size()) {
|
||||
totalH += spacingPx;
|
||||
remainingH -= spacingPx;
|
||||
}
|
||||
}
|
||||
|
||||
desiredSize = {
|
||||
(width_.mode == Length::Mode::Auto) ? maxChildW + p.Horiz() : ownW,
|
||||
(height_.mode == Length::Mode::Auto) ? totalH + p.Vert() : ownH,
|
||||
};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& ctx) override {
|
||||
computedRect = rect;
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
Rect content = ShrinkBy(rect, p);
|
||||
float spacingPx = spacing_ * ctx.scale;
|
||||
|
||||
// First pass: sum fixed heights + count Frac weight.
|
||||
float fracWeight = 0;
|
||||
float fixedH = 0;
|
||||
for (auto& c : children_) {
|
||||
if (c->height_.mode == Length::Mode::Frac) {
|
||||
fracWeight += c->height_.value;
|
||||
} else {
|
||||
fixedH += c->desiredSize.h;
|
||||
}
|
||||
}
|
||||
if (children_.size() > 1) fixedH += spacingPx * (children_.size() - 1);
|
||||
float leftover = std::max(0.0f, content.h - fixedH);
|
||||
float fracUnit = (fracWeight > 0) ? (leftover / fracWeight) : 0;
|
||||
|
||||
// Second pass: arrange.
|
||||
float y = content.y;
|
||||
for (std::size_t i = 0; i < children_.size(); ++i) {
|
||||
auto& c = *children_[i];
|
||||
float h = (c.height_.mode == Length::Mode::Frac)
|
||||
? c.height_.value * fracUnit
|
||||
: c.desiredSize.h;
|
||||
float w = detail::ResolveCrossAxis(c, c.width_, content.w, ctx.scale, c.desiredSize.w);
|
||||
if (c.width_.mode == Length::Mode::Frac) w = content.w * c.width_.value;
|
||||
if (w > content.w) w = content.w;
|
||||
|
||||
// Cross-axis (horizontal) alignment: honor the child's anchor
|
||||
// for the horizontal half (Left/Center/Right); default Left.
|
||||
float x = content.x;
|
||||
if (c.anchor_) {
|
||||
switch (*c.anchor_) {
|
||||
case Anchor::Top: case Anchor::Center: case Anchor::Bottom:
|
||||
x = content.x + (content.w - w) / 2; break;
|
||||
case Anchor::TopRight: case Anchor::Right: case Anchor::BottomRight:
|
||||
x = content.x + content.w - w; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
c.Arrange({x, y, w, h}, ctx);
|
||||
y += h + spacingPx;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────── HStack ───────────────────────────────────
|
||||
struct HStack : WidgetBuilder<HStack> {
|
||||
float spacing_ = 0;
|
||||
|
||||
HStack& spacing(float s) { spacing_ = s; return *this; }
|
||||
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
float spacingPx = spacing_ * ctx.scale;
|
||||
|
||||
float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; });
|
||||
float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; });
|
||||
float contentW = std::max(0.0f, ownW - p.Horiz());
|
||||
float contentH = std::max(0.0f, ownH - p.Vert());
|
||||
|
||||
float maxChildH = 0;
|
||||
float totalW = 0;
|
||||
float remainingW = contentW;
|
||||
for (std::size_t i = 0; i < children_.size(); ++i) {
|
||||
auto& c = *children_[i];
|
||||
Size childAvail = { std::max(0.0f, remainingW), contentH };
|
||||
Size cs = c.Measure(childAvail, ctx);
|
||||
maxChildH = std::max(maxChildH, cs.h);
|
||||
totalW += cs.w;
|
||||
remainingW -= cs.w;
|
||||
if (i + 1 < children_.size()) {
|
||||
totalW += spacingPx;
|
||||
remainingW -= spacingPx;
|
||||
}
|
||||
}
|
||||
|
||||
desiredSize = {
|
||||
(width_.mode == Length::Mode::Auto) ? totalW + p.Horiz() : ownW,
|
||||
(height_.mode == Length::Mode::Auto) ? maxChildH + p.Vert() : ownH,
|
||||
};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& ctx) override {
|
||||
computedRect = rect;
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
Rect content = ShrinkBy(rect, p);
|
||||
float spacingPx = spacing_ * ctx.scale;
|
||||
|
||||
float fracWeight = 0;
|
||||
float fixedW = 0;
|
||||
for (auto& c : children_) {
|
||||
if (c->width_.mode == Length::Mode::Frac) {
|
||||
fracWeight += c->width_.value;
|
||||
} else {
|
||||
fixedW += c->desiredSize.w;
|
||||
}
|
||||
}
|
||||
if (children_.size() > 1) fixedW += spacingPx * (children_.size() - 1);
|
||||
float leftover = std::max(0.0f, content.w - fixedW);
|
||||
float fracUnit = (fracWeight > 0) ? (leftover / fracWeight) : 0;
|
||||
|
||||
float x = content.x;
|
||||
for (std::size_t i = 0; i < children_.size(); ++i) {
|
||||
auto& c = *children_[i];
|
||||
float w = (c.width_.mode == Length::Mode::Frac)
|
||||
? c.width_.value * fracUnit
|
||||
: c.desiredSize.w;
|
||||
float h = detail::ResolveCrossAxis(c, c.height_, content.h, ctx.scale, c.desiredSize.h);
|
||||
if (c.height_.mode == Length::Mode::Frac) h = content.h * c.height_.value;
|
||||
if (h > content.h) h = content.h;
|
||||
|
||||
float y = content.y;
|
||||
if (c.anchor_) {
|
||||
switch (*c.anchor_) {
|
||||
case Anchor::Left: case Anchor::Center: case Anchor::Right:
|
||||
y = content.y + (content.h - h) / 2; break;
|
||||
case Anchor::BottomLeft: case Anchor::Bottom: case Anchor::BottomRight:
|
||||
y = content.y + content.h - h; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
c.Arrange({x, y, w, h}, ctx);
|
||||
x += w + spacingPx;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ──────────────────── Stack (anchored layering) ───────────────────────
|
||||
// Children stack on top of each other inside the parent's content rect.
|
||||
// Each child positions itself by its own `.anchor(...)`; default is
|
||||
// TopLeft. Children with Auto sizes are sized to their measured needs.
|
||||
struct Stack : WidgetBuilder<Stack> {
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; });
|
||||
float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; });
|
||||
float contentW = std::max(0.0f, ownW - p.Horiz());
|
||||
float contentH = std::max(0.0f, ownH - p.Vert());
|
||||
|
||||
float maxW = 0, maxH = 0;
|
||||
for (auto& c : children_) {
|
||||
Size cs = c->Measure({contentW, contentH}, ctx);
|
||||
maxW = std::max(maxW, cs.w);
|
||||
maxH = std::max(maxH, cs.h);
|
||||
}
|
||||
|
||||
desiredSize = {
|
||||
(width_.mode == Length::Mode::Auto) ? maxW + p.Horiz() : ownW,
|
||||
(height_.mode == Length::Mode::Auto) ? maxH + p.Vert() : ownH,
|
||||
};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& ctx) override {
|
||||
computedRect = rect;
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
Rect content = ShrinkBy(rect, p);
|
||||
|
||||
for (auto& cp : children_) {
|
||||
auto& c = *cp;
|
||||
// Resolve child width/height. Frac fills parent.
|
||||
float w = (c.width_.mode == Length::Mode::Auto)
|
||||
? c.desiredSize.w
|
||||
: (c.width_.mode == Length::Mode::Frac
|
||||
? content.w * c.width_.value
|
||||
: ResolveLength(c.width_, content.w, ctx.scale, [&]{return c.desiredSize.w;}));
|
||||
float h = (c.height_.mode == Length::Mode::Auto)
|
||||
? c.desiredSize.h
|
||||
: (c.height_.mode == Length::Mode::Frac
|
||||
? content.h * c.height_.value
|
||||
: ResolveLength(c.height_, content.h, ctx.scale, [&]{return c.desiredSize.h;}));
|
||||
w = std::min(w, content.w);
|
||||
h = std::min(h, content.h);
|
||||
|
||||
Anchor a = c.anchor_.value_or(Anchor::TopLeft);
|
||||
float x = content.x, y = content.y;
|
||||
switch (a) {
|
||||
case Anchor::TopLeft: break;
|
||||
case Anchor::Top: x = content.x + (content.w - w) / 2; break;
|
||||
case Anchor::TopRight: x = content.x + content.w - w; break;
|
||||
case Anchor::Left: y = content.y + (content.h - h) / 2; break;
|
||||
case Anchor::Center: x = content.x + (content.w - w) / 2;
|
||||
y = content.y + (content.h - h) / 2; break;
|
||||
case Anchor::Right: x = content.x + content.w - w;
|
||||
y = content.y + (content.h - h) / 2; break;
|
||||
case Anchor::BottomLeft: y = content.y + content.h - h; break;
|
||||
case Anchor::Bottom: x = content.x + (content.w - w) / 2;
|
||||
y = content.y + content.h - h; break;
|
||||
case Anchor::BottomRight: x = content.x + content.w - w;
|
||||
y = content.y + content.h - h; break;
|
||||
}
|
||||
c.Arrange({x, y, w, h}, ctx);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Overlay is functionally identical to Stack today; it exists so user
|
||||
// code can spell intent ("layered HUD" vs "anchored content").
|
||||
using Overlay = Stack;
|
||||
|
||||
// ─────────────────────────── TextRun ──────────────────────────────────
|
||||
// A styled span inside a Text widget. Per-run overrides win over the
|
||||
// Text's base style; unset fields fall back to the parent Text.
|
||||
struct TextRun {
|
||||
std::string text;
|
||||
std::optional<float> size_;
|
||||
std::optional<Color> color_;
|
||||
bool bold_ = false;
|
||||
bool italic_ = false;
|
||||
bool underline_ = false;
|
||||
bool strikethrough_ = false;
|
||||
|
||||
TextRun() = default;
|
||||
TextRun(std::string s) : text(std::move(s)) {}
|
||||
TextRun(const char* s) : text(s) {}
|
||||
|
||||
TextRun& size(float s) { size_ = s; return *this; }
|
||||
TextRun& color(Color c) { color_ = c; return *this; }
|
||||
TextRun& bold() { bold_ = true; return *this; }
|
||||
TextRun& italic() { italic_ = true; return *this; }
|
||||
TextRun& underline() { underline_ = true; return *this; }
|
||||
TextRun& strikethrough() { strikethrough_ = true; return *this; }
|
||||
};
|
||||
|
||||
// ─────────────────────────── Text ─────────────────────────────────────
|
||||
// Static text. V1 supports a single line, single font, with per-run
|
||||
// styling (color, size, weight, italics, underline, strikethrough).
|
||||
// Wrap, multi-font, BiDi etc. are V2+.
|
||||
struct Text : WidgetBuilder<Text> {
|
||||
// Bring back the layout `size(Length, Length)` overload — our own
|
||||
// `size(float)` would otherwise hide it.
|
||||
using WidgetBuilder<Text>::size;
|
||||
|
||||
std::vector<TextRun> runs_;
|
||||
Font* font_ = nullptr;
|
||||
float size_ = 16.0f;
|
||||
Color color_{1, 1, 1, 1};
|
||||
|
||||
Text() = default;
|
||||
Text(std::string s) { runs_.emplace_back(std::move(s)); }
|
||||
Text(const char* s) { runs_.emplace_back(std::string(s)); }
|
||||
|
||||
Text& font(Font& f) { font_ = &f; return *this; }
|
||||
Text& size(float s) { size_ = s; return *this; }
|
||||
Text& color(Color c) { color_ = c; return *this; }
|
||||
|
||||
// Replace the run list with a parameter pack of styled runs.
|
||||
template<typename... Rs>
|
||||
requires (std::convertible_to<std::decay_t<Rs>, TextRun> && ...)
|
||||
Text& runs(Rs&&... rs) {
|
||||
runs_.clear();
|
||||
runs_.reserve(sizeof...(Rs));
|
||||
(runs_.emplace_back(std::forward<Rs>(rs)), ...);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{
|
||||
if (!font_) return 0.0f;
|
||||
float w = 0;
|
||||
for (auto& r : runs_) {
|
||||
float rs = (r.size_.value_or(size_)) * ctx.scale;
|
||||
w += font_->GetLineWidth(r.text, rs);
|
||||
}
|
||||
return w;
|
||||
});
|
||||
float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{
|
||||
if (!font_) return 0.0f;
|
||||
// Tallest run dictates the line height.
|
||||
float h = 0;
|
||||
for (auto& r : runs_) {
|
||||
float rs = (r.size_.value_or(size_)) * ctx.scale;
|
||||
h = std::max(h, font_->LineHeight(rs));
|
||||
}
|
||||
return h;
|
||||
});
|
||||
desiredSize = {ownW, ownH};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& /*ctx*/) override {
|
||||
computedRect = rect;
|
||||
}
|
||||
|
||||
void Emit(DrawList& dl) const override {
|
||||
if (!font_) return;
|
||||
float cursorX = computedRect.x;
|
||||
for (auto& r : runs_) {
|
||||
float rs = r.size_.value_or(size_) * dl.scale;
|
||||
Color c = r.color_.value_or(color_);
|
||||
cursorX = detail::EmitText(dl, font_, r.text, rs, c, cursorX, computedRect.y);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────── Image ────────────────────────────────────
|
||||
// A texture-or-asset reference. V1 stores just the source path; the
|
||||
// renderer will resolve it. If `sourceSize_` is set, an Auto axis paired
|
||||
// with a fixed axis preserves aspect ratio.
|
||||
struct Image : WidgetBuilder<Image> {
|
||||
std::filesystem::path source_;
|
||||
Size sourceSize_{};
|
||||
Color tint_{1, 1, 1, 1};
|
||||
|
||||
Image() = default;
|
||||
Image(std::filesystem::path p) : source_(std::move(p)) {}
|
||||
Image(const char* p) : source_(p) {}
|
||||
|
||||
Image& source(std::filesystem::path p) { source_ = std::move(p); return *this; }
|
||||
Image& sourceSize(Size s) { sourceSize_ = s; return *this; }
|
||||
Image& tint(Color c) { tint_ = c; return *this; }
|
||||
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
float w = ResolveLength(width_, avail.w, ctx.scale,
|
||||
[&]{ return sourceSize_.w * ctx.scale; });
|
||||
float h = ResolveLength(height_, avail.h, ctx.scale,
|
||||
[&]{ return sourceSize_.h * ctx.scale; });
|
||||
|
||||
// If we know the source aspect AND only one axis is Auto, derive
|
||||
// the missing axis to preserve aspect ratio.
|
||||
if (sourceSize_.w > 0 && sourceSize_.h > 0) {
|
||||
bool autoW = (width_.mode == Length::Mode::Auto);
|
||||
bool autoH = (height_.mode == Length::Mode::Auto);
|
||||
if (autoW && !autoH && h > 0) w = h * (sourceSize_.w / sourceSize_.h);
|
||||
else if (autoH && !autoW && w > 0) h = w * (sourceSize_.h / sourceSize_.w);
|
||||
}
|
||||
desiredSize = {w, h};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& /*ctx*/) override {
|
||||
computedRect = rect;
|
||||
}
|
||||
|
||||
std::uint32_t bindlessSlot_ = 0; // assigned by UIScene when source is loaded
|
||||
|
||||
void Emit(DrawList& dl) const override {
|
||||
if (bindlessSlot_ == 0) return; // texture not loaded yet
|
||||
dl.AddImage(computedRect, tint_, bindlessSlot_);
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────── ProgressBar ──────────────────────────────
|
||||
// Horizontal bar showing `value_` ∈ [min_, max_]. `bindValue` registers
|
||||
// an Observable so external state drives the bar without rebuilding.
|
||||
struct ProgressBar : WidgetBuilder<ProgressBar> {
|
||||
// Bring back the layout `size(Length, Length)` overload — the
|
||||
// single-arg `value(...)` sets a float; layout sizing uses Length.
|
||||
using WidgetBuilder<ProgressBar>::size;
|
||||
|
||||
float value_ = 0.0f;
|
||||
float min_ = 0.0f;
|
||||
float max_ = 1.0f;
|
||||
Color background_{0.20f, 0.20f, 0.20f, 1.0f};
|
||||
Color foreground_{0.40f, 0.70f, 1.00f, 1.0f};
|
||||
Observable<float>* boundValue_ = nullptr;
|
||||
|
||||
ProgressBar() { height_ = Length::Px(8); }
|
||||
|
||||
ProgressBar& value(float v) { value_ = v; return *this; }
|
||||
ProgressBar& range(float lo, float hi) { min_ = lo; max_ = hi; return *this; }
|
||||
ProgressBar& background(Color c) { background_ = c; return *this; }
|
||||
ProgressBar& foreground(Color c) { foreground_ = c; return *this; }
|
||||
ProgressBar& bindValue(Observable<float>& v, float lo = 0.0f, float hi = 1.0f) {
|
||||
boundValue_ = &v; min_ = lo; max_ = hi; return *this;
|
||||
}
|
||||
|
||||
// Normalised progress in [0, 1].
|
||||
float Progress() const {
|
||||
float v = boundValue_ ? boundValue_->Get() : value_;
|
||||
if (max_ <= min_) return 0.0f;
|
||||
return std::clamp((v - min_) / (max_ - min_), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
float w = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; });
|
||||
float h = ResolveLength(height_, avail.h, ctx.scale, [&]{ return 8.0f * ctx.scale; });
|
||||
desiredSize = {w, h};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& /*ctx*/) override {
|
||||
computedRect = rect;
|
||||
}
|
||||
|
||||
void Emit(DrawList& dl) const override {
|
||||
dl.AddRect(computedRect, background_);
|
||||
float p = Progress();
|
||||
if (p > 0.0f) {
|
||||
Rect fg{computedRect.x, computedRect.y, computedRect.w * p, computedRect.h};
|
||||
dl.AddRect(fg, foreground_);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────── ButtonStyle ─────────────────────────────
|
||||
// Reusable visual style for Buttons. Lives in UIWidgets (not UITheme)
|
||||
// so Button::style(...) can take it by const-ref without a circular
|
||||
// module dependency.
|
||||
struct ButtonStyle {
|
||||
Color background {0.20f, 0.20f, 0.20f, 1.0f};
|
||||
Color hoverBackground {0.28f, 0.28f, 0.28f, 1.0f};
|
||||
Color pressedBackground{0.14f, 0.14f, 0.14f, 1.0f};
|
||||
Color textColor {0.95f, 0.95f, 0.95f, 1.0f};
|
||||
Color borderColor {0.30f, 0.30f, 0.30f, 1.0f};
|
||||
float fontSize = 16.0f;
|
||||
Edges padding{8.0f, 12.0f};
|
||||
};
|
||||
|
||||
// ─────────────────────────── InputFieldStyle ─────────────────────────
|
||||
struct InputFieldStyle {
|
||||
Color background {0.10f, 0.10f, 0.10f, 1.0f};
|
||||
Color textColor {0.95f, 0.95f, 0.95f, 1.0f};
|
||||
Color borderColor {0.40f, 0.40f, 0.40f, 1.0f};
|
||||
Color focusBorderColor {0.40f, 0.70f, 1.00f, 1.0f};
|
||||
float fontSize = 16.0f;
|
||||
Edges padding{6.0f, 8.0f};
|
||||
};
|
||||
|
||||
// ─────────────────────────── InputField ──────────────────────────────
|
||||
// Single-line text editor. V1 stores a std::string; the renderer draws
|
||||
// the background, border, text, and a focus-cursor caret. Keyboard
|
||||
// events are wired in by UI-Hit/UI-Scene; for now the widget owns the
|
||||
// data + visual config and exposes onChange/onSubmit callbacks.
|
||||
struct InputField : WidgetBuilder<InputField> {
|
||||
std::string text_;
|
||||
Font* font_ = nullptr;
|
||||
float fontSize_ = 16.0f;
|
||||
Color textColor_{0.95f, 0.95f, 0.95f, 1.0f};
|
||||
Color background_{0.10f, 0.10f, 0.10f, 1.0f};
|
||||
Color borderColor_{0.40f, 0.40f, 0.40f, 1.0f};
|
||||
Color focusBorderColor_{0.40f, 0.70f, 1.00f, 1.0f};
|
||||
bool focused_ = false;
|
||||
std::size_t cursor_ = 0; // codepoint index within text_
|
||||
std::string placeholder_;
|
||||
std::function<void(const std::string&)> onChange_;
|
||||
std::function<void(const std::string&)> onSubmit_;
|
||||
|
||||
InputField() {
|
||||
padding_ = Edges(6, 8);
|
||||
width_ = Length::Px(160);
|
||||
}
|
||||
InputField(std::string initial) : InputField() {
|
||||
text_ = std::move(initial);
|
||||
cursor_ = text_.size();
|
||||
}
|
||||
|
||||
InputField& text(std::string s) { text_ = std::move(s); cursor_ = text_.size(); return *this; }
|
||||
InputField& placeholder(std::string s) { placeholder_ = std::move(s); return *this; }
|
||||
InputField& font(Font& f) { font_ = &f; return *this; }
|
||||
InputField& fontSize(float s) { fontSize_ = s; return *this; }
|
||||
InputField& textColor(Color c) { textColor_ = c; return *this; }
|
||||
InputField& background(Color c) { background_ = c; return *this; }
|
||||
InputField& borderColor(Color c) { borderColor_ = c; return *this; }
|
||||
InputField& focusBorderColor(Color c) { focusBorderColor_ = c; return *this; }
|
||||
InputField& onChange(std::function<void(const std::string&)> f) { onChange_ = std::move(f); return *this; }
|
||||
InputField& onSubmit(std::function<void(const std::string&)> f) { onSubmit_ = std::move(f); return *this; }
|
||||
|
||||
InputField& style(const InputFieldStyle& s) {
|
||||
background_ = s.background;
|
||||
textColor_ = s.textColor;
|
||||
borderColor_ = s.borderColor;
|
||||
focusBorderColor_ = s.focusBorderColor;
|
||||
fontSize_ = s.fontSize;
|
||||
padding_ = s.padding;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
float devSize = fontSize_ * ctx.scale;
|
||||
float lineH = font_ ? font_->LineHeight(devSize) : devSize;
|
||||
|
||||
float w = ResolveLength(width_, avail.w, ctx.scale, [&]{ return 160.0f * ctx.scale; });
|
||||
float h = ResolveLength(height_, avail.h, ctx.scale, [&]{ return lineH + p.Vert(); });
|
||||
desiredSize = {w, h};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& /*ctx*/) override {
|
||||
computedRect = rect;
|
||||
}
|
||||
|
||||
// Interaction. UIScene's focus manager flips `focused_` via
|
||||
// OnFocus/OnBlur; mouse clicks just need to be claimed so the
|
||||
// bubble stops here (and so a non-focusable parent doesn't eat
|
||||
// the event). The text-edit ops are deliberately tiny: insert at
|
||||
// cursor, backspace, delete, enter; arrow keys move the caret.
|
||||
bool IsFocusable() const override { return true; }
|
||||
void OnFocus() override { focused_ = true; }
|
||||
void OnBlur() override { focused_ = false; }
|
||||
bool OnMouseClick(float, float) override { return true; }
|
||||
|
||||
bool OnTextInput(std::string_view text) override {
|
||||
text_.insert(cursor_, text);
|
||||
cursor_ += text.size();
|
||||
if (onChange_) onChange_(text_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OnKeyDown(CrafterKeys key) override {
|
||||
switch (key) {
|
||||
case CrafterKeys::Backspace: {
|
||||
if (cursor_ > 0) {
|
||||
// Remove a full UTF-8 codepoint, not a byte.
|
||||
std::size_t back = 1;
|
||||
while (back < cursor_ &&
|
||||
(static_cast<unsigned char>(text_[cursor_ - back]) & 0xC0) == 0x80) {
|
||||
++back;
|
||||
}
|
||||
text_.erase(cursor_ - back, back);
|
||||
cursor_ -= back;
|
||||
if (onChange_) onChange_(text_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case CrafterKeys::Delete: {
|
||||
if (cursor_ < text_.size()) {
|
||||
std::size_t fwd = 1;
|
||||
while (cursor_ + fwd < text_.size() &&
|
||||
(static_cast<unsigned char>(text_[cursor_ + fwd]) & 0xC0) == 0x80) {
|
||||
++fwd;
|
||||
}
|
||||
text_.erase(cursor_, fwd);
|
||||
if (onChange_) onChange_(text_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case CrafterKeys::Left: {
|
||||
while (cursor_ > 0) {
|
||||
--cursor_;
|
||||
if ((static_cast<unsigned char>(text_[cursor_]) & 0xC0) != 0x80) break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case CrafterKeys::Right: {
|
||||
while (cursor_ < text_.size()) {
|
||||
++cursor_;
|
||||
if (cursor_ == text_.size() ||
|
||||
(static_cast<unsigned char>(text_[cursor_]) & 0xC0) != 0x80) break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case CrafterKeys::Home: cursor_ = 0; return true;
|
||||
case CrafterKeys::End: cursor_ = text_.size(); return true;
|
||||
case CrafterKeys::Enter:
|
||||
if (onSubmit_) onSubmit_(text_);
|
||||
return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Emit(DrawList& dl) const override {
|
||||
// Background.
|
||||
dl.AddRect(computedRect, background_);
|
||||
|
||||
// Border: 4 thin rects so the user can see focus state without
|
||||
// a stencil pass. 1-device-pixel-wide outline.
|
||||
float t = std::max(1.0f, dl.scale);
|
||||
Color border = focused_ ? focusBorderColor_ : borderColor_;
|
||||
dl.AddRect({computedRect.x, computedRect.y, computedRect.w, t}, border); // top
|
||||
dl.AddRect({computedRect.x, computedRect.y + computedRect.h - t, computedRect.w, t}, border); // bottom
|
||||
dl.AddRect({computedRect.x, computedRect.y, t, computedRect.h}, border); // left
|
||||
dl.AddRect({computedRect.x + computedRect.w - t, computedRect.y, t, computedRect.h}, border); // right
|
||||
|
||||
if (font_) {
|
||||
EdgesPx p = ResolveEdges(padding_, dl.scale);
|
||||
float devSize = fontSize_ * dl.scale;
|
||||
float originX = computedRect.x + p.left;
|
||||
float originY = computedRect.y + p.top;
|
||||
std::string_view show = !text_.empty() ? std::string_view(text_)
|
||||
: std::string_view(placeholder_);
|
||||
Color col = !text_.empty() ? textColor_
|
||||
: Color{textColor_.r, textColor_.g, textColor_.b, textColor_.a * 0.5f};
|
||||
detail::EmitText(dl, font_, show, devSize, col, originX, originY);
|
||||
|
||||
// Caret bar at the cursor position when focused.
|
||||
if (focused_) {
|
||||
std::string_view before(text_.data(), cursor_);
|
||||
float prefixW = static_cast<float>(font_->GetLineWidth(before, devSize));
|
||||
float caretX = originX + prefixW;
|
||||
dl.AddRect({caretX, originY, t, font_->LineHeight(devSize)}, focusBorderColor_);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────── ScrollView ──────────────────────────────
|
||||
// Clipping viewport. Children are laid out vertically (treated like a
|
||||
// VStack with no spacing) and translated by scrollY_; horizontal scroll
|
||||
// is opt-in via .horizontal(true). Hit/wheel/drag interaction is wired
|
||||
// by UI-Hit/UI-Scene.
|
||||
struct ScrollView : WidgetBuilder<ScrollView> {
|
||||
bool scrollVertical_ = true;
|
||||
bool scrollHorizontal_ = false;
|
||||
float scrollX_ = 0.0f;
|
||||
float scrollY_ = 0.0f;
|
||||
Size contentSize_{}; // total laid-out children size (for scroll bounds)
|
||||
|
||||
ScrollView& vertical(bool b) { scrollVertical_ = b; return *this; }
|
||||
ScrollView& horizontal(bool b) { scrollHorizontal_ = b; return *this; }
|
||||
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
|
||||
// Viewport size — defaults to filling available.
|
||||
float w = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; });
|
||||
float h = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; });
|
||||
|
||||
// Children measure with effectively-unbounded space along the
|
||||
// scroll axis, allowing them to grow beyond the viewport.
|
||||
constexpr float kInf = std::numeric_limits<float>::max();
|
||||
Size childAvail = {
|
||||
scrollHorizontal_ ? kInf : std::max(0.0f, w - p.Horiz()),
|
||||
scrollVertical_ ? kInf : std::max(0.0f, h - p.Vert()),
|
||||
};
|
||||
float totalH = 0;
|
||||
float maxW = 0;
|
||||
for (auto& c : children_) {
|
||||
Size cs = c->Measure(childAvail, ctx);
|
||||
totalH += cs.h;
|
||||
maxW = std::max(maxW, cs.w);
|
||||
}
|
||||
contentSize_ = {maxW, totalH};
|
||||
|
||||
desiredSize = {w, h};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& ctx) override {
|
||||
computedRect = rect;
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
Rect content = ShrinkBy(rect, p);
|
||||
|
||||
// Clamp scroll to valid range.
|
||||
float maxScrollY = std::max(0.0f, contentSize_.h - content.h);
|
||||
float maxScrollX = std::max(0.0f, contentSize_.w - content.w);
|
||||
scrollY_ = std::clamp(scrollY_, 0.0f, maxScrollY);
|
||||
scrollX_ = std::clamp(scrollX_, 0.0f, maxScrollX);
|
||||
|
||||
float y = content.y - scrollY_;
|
||||
for (auto& c : children_) {
|
||||
float w = (c->width_.mode == Length::Mode::Auto)
|
||||
? c->desiredSize.w
|
||||
: ResolveLength(c->width_, content.w, ctx.scale,
|
||||
[&]{ return c->desiredSize.w; });
|
||||
float h = c->desiredSize.h;
|
||||
c->Arrange({content.x - scrollX_, y, w, h}, ctx);
|
||||
y += h;
|
||||
}
|
||||
}
|
||||
|
||||
void Emit(DrawList& dl) const override {
|
||||
// Wrap children in a clip rect equal to our viewport.
|
||||
dl.PushClip(computedRect);
|
||||
for (auto& c : children_) c->Emit(dl);
|
||||
dl.PopClip();
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────── TabView ──────────────────────────────────
|
||||
// A tab bar at the top + the selected tab's content below. Each `.tab(
|
||||
// name, widget)` adds a child widget; the selected index drives which
|
||||
// child is laid out and rendered. Tab clicks are routed by UI-Hit later.
|
||||
struct TabView : WidgetBuilder<TabView> {
|
||||
std::vector<std::string> tabNames_;
|
||||
int selected_ = 0;
|
||||
Font* font_ = nullptr;
|
||||
float tabFontSize_ = 14.0f;
|
||||
Color tabBackground_{0.10f, 0.10f, 0.10f, 1.0f};
|
||||
Color tabActiveBackground_{0.18f, 0.18f, 0.18f, 1.0f};
|
||||
Color tabTextColor_{0.85f, 0.85f, 0.85f, 1.0f};
|
||||
Color tabActiveTextColor_{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
float tabBarHeight_ = 32.0f;
|
||||
float tabBarBottomYPx_ = 0.0f; // cached after Arrange for hit-testing
|
||||
|
||||
TabView& font(Font& f) { font_ = &f; return *this; }
|
||||
TabView& tabFontSize(float s) { tabFontSize_ = s; return *this; }
|
||||
TabView& tabBarHeight(float h) { tabBarHeight_ = h; return *this; }
|
||||
TabView& selected(int i) { selected_ = i; return *this; }
|
||||
|
||||
// Appends one tab (name + content widget). Each content widget
|
||||
// is owned via children_; tabNames_[i] mirrors children_[i]'s name.
|
||||
template<typename W>
|
||||
requires std::derived_from<std::remove_cvref_t<W>, Widget>
|
||||
TabView& tab(std::string name, W&& content) {
|
||||
tabNames_.push_back(std::move(name));
|
||||
auto p = std::make_unique<std::remove_cvref_t<W>>(std::move(content));
|
||||
p->parent = this;
|
||||
children_.push_back(std::move(p));
|
||||
return *this;
|
||||
}
|
||||
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
float tbh = tabBarHeight_ * ctx.scale;
|
||||
|
||||
float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; });
|
||||
float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; });
|
||||
Size contentAvail = {
|
||||
std::max(0.0f, ownW - p.Horiz()),
|
||||
std::max(0.0f, ownH - p.Vert() - tbh),
|
||||
};
|
||||
|
||||
// Measure ALL tab contents (so a switch doesn't trigger a
|
||||
// re-measure). Cheap for typical tab counts.
|
||||
for (auto& c : children_) c->Measure(contentAvail, ctx);
|
||||
|
||||
desiredSize = {ownW, ownH};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& ctx) override {
|
||||
computedRect = rect;
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
Rect content = ShrinkBy(rect, p);
|
||||
float tbh = tabBarHeight_ * ctx.scale;
|
||||
tabBarBottomYPx_ = content.y + tbh;
|
||||
|
||||
// The active tab gets the area below the tab bar.
|
||||
Rect contentArea = {
|
||||
content.x,
|
||||
content.y + tbh,
|
||||
content.w,
|
||||
std::max(0.0f, content.h - tbh),
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i < children_.size(); ++i) {
|
||||
if (static_cast<int>(i) == selected_) {
|
||||
children_[i]->Arrange(contentArea, ctx);
|
||||
} else {
|
||||
// Off-screen so nothing draws / hit-tests for inactive tabs.
|
||||
children_[i]->Arrange({0, 0, 0, 0}, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool OnMouseClick(float x, float y) override {
|
||||
// Only the tab bar consumes clicks; let content-area clicks bubble.
|
||||
if (y < computedRect.y || y >= tabBarBottomYPx_) return false;
|
||||
if (tabNames_.empty()) return false;
|
||||
float tabW = computedRect.w / static_cast<float>(tabNames_.size());
|
||||
int idx = static_cast<int>((x - computedRect.x) / tabW);
|
||||
idx = std::clamp(idx, 0, static_cast<int>(tabNames_.size()) - 1);
|
||||
selected_ = idx;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Emit(DrawList& dl) const override {
|
||||
if (tabNames_.empty()) return;
|
||||
|
||||
float tbh = tabBarBottomYPx_ - computedRect.y;
|
||||
// Whole tab-bar background.
|
||||
dl.AddRect({computedRect.x, computedRect.y, computedRect.w, tbh}, tabBackground_);
|
||||
|
||||
// Active-tab highlight + labels.
|
||||
float tabW = computedRect.w / static_cast<float>(tabNames_.size());
|
||||
for (std::size_t i = 0; i < tabNames_.size(); ++i) {
|
||||
float tx = computedRect.x + tabW * static_cast<float>(i);
|
||||
bool active = (static_cast<int>(i) == selected_);
|
||||
if (active) {
|
||||
dl.AddRect({tx, computedRect.y, tabW, tbh}, tabActiveBackground_);
|
||||
}
|
||||
if (font_) {
|
||||
float devSize = tabFontSize_ * dl.scale;
|
||||
float labelW = static_cast<float>(font_->GetLineWidth(tabNames_[i], devSize));
|
||||
float labelX = tx + (tabW - labelW) * 0.5f;
|
||||
float labelY = computedRect.y + (tbh - font_->LineHeight(devSize)) * 0.5f;
|
||||
Color col = active ? tabActiveTextColor_ : tabTextColor_;
|
||||
detail::EmitText(dl, font_, tabNames_[i], devSize, col, labelX, labelY);
|
||||
}
|
||||
}
|
||||
|
||||
// Active tab content only.
|
||||
if (selected_ >= 0 && selected_ < static_cast<int>(children_.size())) {
|
||||
children_[selected_]->Emit(dl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────── Button ───────────────────────────────────
|
||||
// Clickable rectangle with a label. Default padding makes the click
|
||||
// target comfortable; users can override via .padding(...).
|
||||
struct Button : WidgetBuilder<Button> {
|
||||
std::string label_;
|
||||
std::function<void()> onClick_;
|
||||
Color background_{0.2f, 0.2f, 0.2f, 1.0f};
|
||||
Color textColor_{1, 1, 1, 1};
|
||||
Font* font_ = nullptr;
|
||||
float fontSize_ = 16.0f;
|
||||
|
||||
Button() { padding_ = Edges(8, 12); }
|
||||
Button(std::string l) : Button() { label_ = std::move(l); }
|
||||
Button(const char* l) : Button() { label_ = l; }
|
||||
|
||||
Button& text(std::string s) { label_ = std::move(s); return *this; }
|
||||
Button& onClick(std::function<void()> f) { onClick_ = std::move(f); return *this; }
|
||||
Button& background(Color c) { background_ = c; return *this; }
|
||||
Button& textColor(Color c) { textColor_ = c; return *this; }
|
||||
Button& font(Font& f) { font_ = &f; return *this; }
|
||||
Button& fontSize(float s) { fontSize_ = s; return *this; }
|
||||
|
||||
Button& style(const ButtonStyle& s) {
|
||||
background_ = s.background;
|
||||
textColor_ = s.textColor;
|
||||
fontSize_ = s.fontSize;
|
||||
padding_ = s.padding;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
float devSize = fontSize_ * ctx.scale;
|
||||
float textW = (font_ && !label_.empty()) ? font_->GetLineWidth(label_, devSize) : 0.0f;
|
||||
float textH = font_ ? font_->LineHeight(devSize) : devSize;
|
||||
|
||||
float w = ResolveLength(width_, avail.w, ctx.scale, [&]{ return textW + p.Horiz(); });
|
||||
float h = ResolveLength(height_, avail.h, ctx.scale, [&]{ return textH + p.Vert(); });
|
||||
desiredSize = {w, h};
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& /*ctx*/) override {
|
||||
computedRect = rect;
|
||||
}
|
||||
|
||||
bool OnMouseClick(float /*x*/, float /*y*/) override {
|
||||
if (onClick_) { onClick_(); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
void Emit(DrawList& dl) const override {
|
||||
// Rounded background — corner radius scales with DPI.
|
||||
std::uint32_t radius = static_cast<std::uint32_t>(std::round(4.0f * dl.scale));
|
||||
dl.AddRoundRect(computedRect, background_, static_cast<float>(radius));
|
||||
|
||||
// Centred label.
|
||||
if (font_ && !label_.empty()) {
|
||||
float devSize = fontSize_ * dl.scale;
|
||||
float labelW = static_cast<float>(font_->GetLineWidth(label_, devSize));
|
||||
float labelH = font_->LineHeight(devSize);
|
||||
float originX = computedRect.x + (computedRect.w - labelW) * 0.5f;
|
||||
float originY = computedRect.y + (computedRect.h - labelH) * 0.5f;
|
||||
detail::EmitText(dl, font_, label_, devSize, textColor_, originX, originY);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -19,12 +19,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
|
||||
module;
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
|
||||
export module Crafter.Graphics:VulkanBuffer;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
import std;
|
||||
import :Device;
|
||||
|
||||
|
|
@ -213,5 +210,4 @@ namespace Crafter {
|
|||
VulkanBuffer(VulkanBuffer&) = delete;
|
||||
VulkanBuffer& operator=(const VulkanBuffer&) = delete;
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
@ -18,12 +18,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
*/
|
||||
|
||||
module;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#include <assert.h>
|
||||
#endif
|
||||
export module Crafter.Graphics:VulkanTransition;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
import std;
|
||||
|
||||
export namespace Crafter {
|
||||
|
|
@ -189,6 +186,4 @@ export namespace Crafter {
|
|||
static_cast<uint32_t>(image_memory_barriers.size()),
|
||||
image_memory_barriers.data());
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
|
@ -38,9 +38,7 @@ module;
|
|||
#include <wayland-client.h>
|
||||
#include <wayland-client-protocol.h>
|
||||
#endif
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
|
@ -48,23 +46,18 @@ module;
|
|||
export module Crafter.Graphics:Window;
|
||||
import std;
|
||||
import :Types;
|
||||
import :Rendertarget;
|
||||
import :Transform2D;
|
||||
import Crafter.Event;
|
||||
|
||||
export namespace Crafter {
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
struct Semaphores {
|
||||
// Swap chain image presentation
|
||||
VkSemaphore presentComplete;
|
||||
// Command buffer submission and execution
|
||||
VkSemaphore renderComplete;
|
||||
};
|
||||
struct PipelineRTVulkan;
|
||||
struct RenderPass;
|
||||
struct DescriptorHeapVulkan;
|
||||
#endif
|
||||
|
||||
struct MouseElement;
|
||||
struct Window {
|
||||
FrameTime currentFrameTime;
|
||||
std::uint32_t width;
|
||||
|
|
@ -98,9 +91,6 @@ export namespace Crafter {
|
|||
Vector<float, 2> mouseDelta;
|
||||
bool mouseLeftHeld = false;
|
||||
bool mouseRightHeld = false;
|
||||
std::vector<MouseElement*> mouseElements;
|
||||
std::vector<MouseElement*> pendingMouseElements;
|
||||
Rendertarget<std::uint8_t, 4, 4, 1> cursorRenderer;
|
||||
|
||||
Window() = default;
|
||||
Window(std::uint32_t width, std::uint32_t height);
|
||||
|
|
@ -139,16 +129,11 @@ export namespace Crafter {
|
|||
#endif
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
float scale;
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_SOFTWARE
|
||||
Rendertarget<std::uint8_t, 4, 4, 1> renderer;
|
||||
#endif
|
||||
float scale = 1.0f;
|
||||
bool configured = false;
|
||||
xdg_toplevel* xdgToplevel = nullptr;
|
||||
wp_viewport* wpViewport = nullptr;
|
||||
wl_surface* surface = nullptr;
|
||||
wl_buffer* buffer = nullptr;
|
||||
wl_buffer* backBuffer = nullptr;
|
||||
xdg_surface* xdgSurface = nullptr;
|
||||
wl_callback* cb = nullptr;
|
||||
wl_surface* cursorSurface = nullptr;
|
||||
|
|
@ -177,12 +162,17 @@ export namespace Crafter {
|
|||
inline static wp_fractional_scale_v1* wp_scale = nullptr;
|
||||
#endif
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
VkCommandBuffer StartInit();
|
||||
void FinishInit();
|
||||
VkCommandBuffer GetCmd();
|
||||
void EndCmd(VkCommandBuffer cmd);
|
||||
void CreateSwapchain();
|
||||
|
||||
// Save the current swapchain image (state after Render() returns) to
|
||||
// a PNG file. Allocates a one-shot staging buffer + command buffer,
|
||||
// copies image-to-buffer, waits idle, then writes PNG via stb. Useful
|
||||
// for visual regression tests and screenshotting from headless code.
|
||||
void SaveFrame(const std::filesystem::path& path);
|
||||
static constexpr std::uint8_t numFrames = 3;
|
||||
VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE;
|
||||
VkSwapchainKHR swapChain = VK_NULL_HANDLE;
|
||||
|
|
@ -196,8 +186,8 @@ export namespace Crafter {
|
|||
Semaphores semaphores;
|
||||
std::uint32_t currentBuffer = 0;
|
||||
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
PipelineRTVulkan* pipeline;
|
||||
DescriptorHeapVulkan* descriptorHeap;
|
||||
#endif
|
||||
std::vector<RenderPass*> passes;
|
||||
DescriptorHeapVulkan* descriptorHeap = nullptr;
|
||||
std::optional<std::array<float, 4>> clearColor;
|
||||
};
|
||||
}
|
||||
|
|
@ -21,21 +21,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
export module Crafter.Graphics;
|
||||
|
||||
export import :Window;
|
||||
export import :Transform2D;
|
||||
export import :RenderingElement2D;
|
||||
export import :RenderingElement2DBase;
|
||||
export import :MouseElement;
|
||||
export import :GridElement;
|
||||
export import :Types;
|
||||
export import :Device;
|
||||
export import :Font;
|
||||
export import :Animation;
|
||||
export import :Mesh;
|
||||
export import :Rendertarget;
|
||||
export import :ForwardDeclarations;
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
|
||||
export import :Device;
|
||||
export import :VulkanTransition;
|
||||
export import :VulkanBuffer;
|
||||
export import :ShaderVulkan;
|
||||
|
|
@ -45,5 +36,6 @@ export import :RenderingElement3D;
|
|||
export import :ImageVulkan;
|
||||
export import :SamplerVulkan;
|
||||
export import :DescriptorHeapVulkan;
|
||||
export import :RenderingElement2DVulkan;
|
||||
#endif
|
||||
export import :RenderPass;
|
||||
export import :RTPass;
|
||||
export import :UI;
|
||||
Loading…
Add table
Add a link
Reference in a new issue