diff --git a/Dependencies/clay/LICENSE.md b/Dependencies/clay/LICENSE.md new file mode 100644 index 000000000..2968c61bc --- /dev/null +++ b/Dependencies/clay/LICENSE.md @@ -0,0 +1,22 @@ +zlib/libpng license + +Copyright (c) 2024 Nic Barker + +This software is provided 'as-is', without any express or implied warranty. +In no event will the authors be held liable for any damages arising from the +use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. \ No newline at end of file diff --git a/Dependencies/clay/include/clay.h b/Dependencies/clay/include/clay.h new file mode 100644 index 000000000..7d9cf4cc7 --- /dev/null +++ b/Dependencies/clay/include/clay.h @@ -0,0 +1,5058 @@ +// VERSION: 0.14 + +/* + NOTE: In order to use this library you must define + the following macro in exactly one file, _before_ including clay.h: + + #define CLAY_IMPLEMENTATION + #include "clay.h" + + See the examples folder for details. +*/ + +#include +#include +#include + +// SIMD includes on supported platforms +#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64)) +#include +#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__) +#include +#endif +#if __CLION_IDE__ +#define CLAY_IMPLEMENTATION +#endif + +// ----------------------------------------- +// HEADER DECLARATIONS --------------------- +// ----------------------------------------- + +#ifndef CLAY_HEADER +#define CLAY_HEADER + +#if !( \ + (defined(__cplusplus) && __cplusplus >= 202002L) || \ + (defined(__STDC__) && __STDC__ == 1 && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || \ + defined(_MSC_VER) || \ + defined(__OBJC__) \ +) +#error "Clay requires C99, C++20, or MSVC" +#endif + +#ifdef CLAY_WASM +#define CLAY_WASM_EXPORT(name) __attribute__((export_name(name))) +#else +#define CLAY_WASM_EXPORT(null) +#endif + +#ifdef CLAY_DLL +#define CLAY_DLL_EXPORT __declspec(dllexport) __stdcall +#else +#define CLAY_DLL_EXPORT +#endif + +// Public Macro API ------------------------ + +#define CLAY__MAX(x, y) (((x) > (y)) ? (x) : (y)) +#define CLAY__MIN(x, y) (((x) < (y)) ? (x) : (y)) + +#define CLAY_TEXT_CONFIG(...) __VA_ARGS__ + +#define CLAY_BORDER_OUTSIDE(widthValue) {widthValue, widthValue, widthValue, widthValue, 0} + +#define CLAY_BORDER_ALL(widthValue) {widthValue, widthValue, widthValue, widthValue, widthValue} + +#define CLAY_CORNER_RADIUS(radius) (CLAY__INIT(Clay_CornerRadius) { radius, radius, radius, radius }) + +#define CLAY_PADDING_ALL(padding) CLAY__CONFIG_WRAPPER(Clay_Padding, { padding, padding, padding, padding }) + +#define CLAY_SIZING_FIT(...) (CLAY__INIT(Clay_SizingAxis) { .size = { .minMax = { __VA_ARGS__ } }, .type = CLAY__SIZING_TYPE_FIT }) + +#define CLAY_SIZING_GROW(...) (CLAY__INIT(Clay_SizingAxis) { .size = { .minMax = { __VA_ARGS__ } }, .type = CLAY__SIZING_TYPE_GROW }) + +#define CLAY_SIZING_FIXED(fixedSize) (CLAY__INIT(Clay_SizingAxis) { .size = { .minMax = { fixedSize, fixedSize } }, .type = CLAY__SIZING_TYPE_FIXED }) + +#define CLAY_SIZING_PERCENT(percentOfParent) (CLAY__INIT(Clay_SizingAxis) { .size = { .percent = (percentOfParent) }, .type = CLAY__SIZING_TYPE_PERCENT }) + +// Note: If a compile error led you here, you might be trying to use CLAY_ID with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SID instead. +#define CLAY_ID(label) CLAY_SID(CLAY_STRING(label)) + +#define CLAY_SID(label) Clay__HashString(label, 0) + +// Note: If a compile error led you here, you might be trying to use CLAY_IDI with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SIDI instead. +#define CLAY_IDI(label, index) CLAY_SIDI(CLAY_STRING(label), index) + +#define CLAY_SIDI(label, index) Clay__HashStringWithOffset(label, index, 0) + +// Note: If a compile error led you here, you might be trying to use CLAY_ID_LOCAL with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SID_LOCAL instead. +#define CLAY_ID_LOCAL(label) CLAY_SID_LOCAL(CLAY_STRING(label)) + +#define CLAY_SID_LOCAL(label) Clay__HashString(label, Clay_GetOpenElementId()) + +// Note: If a compile error led you here, you might be trying to use CLAY_IDI_LOCAL with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SIDI_LOCAL instead. +#define CLAY_IDI_LOCAL(label, index) CLAY_SIDI_LOCAL(CLAY_STRING(label), index) + +#define CLAY_SIDI_LOCAL(label, index) Clay__HashStringWithOffset(label, index, Clay_GetOpenElementId()) + +#define CLAY__STRING_LENGTH(s) ((sizeof(s) / sizeof((s)[0])) - sizeof((s)[0])) + +#define CLAY__ENSURE_STRING_LITERAL(x) ("" x "") + +// Note: If an error led you here, it's because CLAY_STRING can only be used with string literals, i.e. CLAY_STRING("SomeString") and not CLAY_STRING(yourString) +#define CLAY_STRING(string) (CLAY__INIT(Clay_String) { .isStaticallyAllocated = true, .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) }) + +#define CLAY_STRING_CONST(string) { .isStaticallyAllocated = true, .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) } + +static uint8_t CLAY__ELEMENT_DEFINITION_LATCH; + +// GCC marks the above CLAY__ELEMENT_DEFINITION_LATCH as an unused variable for files that include clay.h but don't declare any layout +// This is to suppress that warning +static inline void Clay__SuppressUnusedLatchDefinitionVariableWarning(void) { (void) CLAY__ELEMENT_DEFINITION_LATCH; } + +// Publicly visible layout element macros ----------------------------------------------------- + +/* This macro looks scary on the surface, but is actually quite simple. + It turns a macro call like this: + + CLAY({ + .id = CLAY_ID("Container"), + .backgroundColor = { 255, 200, 200, 255 } + }) { + ...children declared here + } + + Into calls like this: + + Clay__OpenElement(); + Clay__ConfigureOpenElement((Clay_ElementDeclaration) { + .id = CLAY_ID("Container"), + .backgroundColor = { 255, 200, 200, 255 } + }); + ...children declared here + Clay__CloseElement(); + + The for loop will only ever run a single iteration, putting Clay__CloseElement() in the increment of the loop + means that it will run after the body - where the children are declared. It just exists to make sure you don't forget + to call Clay_CloseElement(). +*/ +#define CLAY_AUTO_ID(...) \ + for ( \ + CLAY__ELEMENT_DEFINITION_LATCH = (Clay__OpenElement(), Clay__ConfigureOpenElement(CLAY__CONFIG_WRAPPER(Clay_ElementDeclaration, __VA_ARGS__)), 0); \ + CLAY__ELEMENT_DEFINITION_LATCH < 1; \ + CLAY__ELEMENT_DEFINITION_LATCH=1, Clay__CloseElement() \ + ) + +#define CLAY(id, ...) \ + for ( \ + CLAY__ELEMENT_DEFINITION_LATCH = (Clay__OpenElementWithId(id), Clay__ConfigureOpenElement(CLAY__CONFIG_WRAPPER(Clay_ElementDeclaration, __VA_ARGS__)), 0); \ + CLAY__ELEMENT_DEFINITION_LATCH < 1; \ + CLAY__ELEMENT_DEFINITION_LATCH=1, Clay__CloseElement() \ + ) + +// These macros exist to allow the CLAY() macro to be called both with an inline struct definition, such as +// CLAY({ .id = something... }); +// As well as by passing a predefined declaration struct +// Clay_ElementDeclaration declarationStruct = ... +// CLAY(declarationStruct); +#define CLAY__WRAPPER_TYPE(type) Clay__##type##Wrapper +#define CLAY__WRAPPER_STRUCT(type) typedef struct { type wrapped; } CLAY__WRAPPER_TYPE(type) +#define CLAY__CONFIG_WRAPPER(type, ...) (CLAY__INIT(CLAY__WRAPPER_TYPE(type)) { __VA_ARGS__ }).wrapped + +#define CLAY_TEXT(text, ...) Clay__OpenTextElement(text, CLAY__CONFIG_WRAPPER(Clay_TextElementConfig, __VA_ARGS__)) + +#ifdef __cplusplus + +#define CLAY__INIT(type) type + +#define CLAY_PACKED_ENUM enum : uint8_t + +#define CLAY__DEFAULT_STRUCT {} + +#else + +#define CLAY__INIT(type) (type) + +#if defined(_MSC_VER) && !defined(__clang__) +#define CLAY_PACKED_ENUM __pragma(pack(push, 1)) enum __pragma(pack(pop)) +#else +#define CLAY_PACKED_ENUM enum __attribute__((__packed__)) +#endif + +#if __STDC_VERSION__ >= 202311L +#define CLAY__DEFAULT_STRUCT {} +#else +#define CLAY__DEFAULT_STRUCT {0} +#endif + +#endif // __cplusplus + +#ifdef __cplusplus +extern "C" { +#endif + +// Utility Structs ------------------------- + +// Note: Clay_String is not guaranteed to be null terminated. It may be if created from a literal C string, +// but it is also used to represent slices. +typedef struct Clay_String { + // Set this boolean to true if the char* data underlying this string will live for the entire lifetime of the program. + // This will automatically be set for strings created with CLAY_STRING, as the macro requires a string literal. + bool isStaticallyAllocated; + int32_t length; + // The underlying character memory. Note: this will not be copied and will not extend the lifetime of the underlying memory. + const char *chars; +} Clay_String; + +// Clay_StringSlice is used to represent non owning string slices, and includes +// a baseChars field which points to the string this slice is derived from. +typedef struct Clay_StringSlice { + int32_t length; + const char *chars; + const char *baseChars; // The source string / char* that this slice was derived from +} Clay_StringSlice; + +typedef struct Clay_Context Clay_Context; + +// Clay_Arena is a memory arena structure that is used by clay to manage its internal allocations. +// Rather than creating it by hand, it's easier to use Clay_CreateArenaWithCapacityAndMemory() +typedef struct Clay_Arena { + uintptr_t nextAllocation; + size_t capacity; + char *memory; +} Clay_Arena; + +typedef struct Clay_Dimensions { + float width, height; +} Clay_Dimensions; + +typedef struct Clay_Vector2 { + float x, y; +} Clay_Vector2; + +// Internally clay conventionally represents colors as 0-255, but interpretation is up to the renderer. +typedef struct Clay_Color { + float r, g, b, a; +} Clay_Color; + +typedef struct Clay_BoundingBox { + float x, y, width, height; +} Clay_BoundingBox; + +// Primarily created via the CLAY_ID(), CLAY_IDI(), CLAY_ID_LOCAL() and CLAY_IDI_LOCAL() macros. +// Represents a hashed string ID used for identifying and finding specific clay UI elements, required +// by functions such as Clay_PointerOver() and Clay_GetElementData(). +typedef struct Clay_ElementId { + uint32_t id; // The resulting hash generated from the other fields. + uint32_t offset; // A numerical offset applied after computing the hash from stringId. + uint32_t baseId; // A base hash value to start from, for example the parent element ID is used when calculating CLAY_ID_LOCAL(). + Clay_String stringId; // The string id to hash. +} Clay_ElementId; + +// A sized array of Clay_ElementId. +typedef struct +{ + int32_t capacity; + int32_t length; + Clay_ElementId *internalArray; +} Clay_ElementIdArray; + +// Controls the "radius", or corner rounding of elements, including rectangles, borders and images. +// The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels. +typedef struct Clay_CornerRadius { + float topLeft; + float topRight; + float bottomLeft; + float bottomRight; +} Clay_CornerRadius; + +// Element Configs --------------------------- + +// Controls the direction in which child elements will be automatically laid out. +typedef CLAY_PACKED_ENUM { + // (Default) Lays out child elements from left to right with increasing x. + CLAY_LEFT_TO_RIGHT, + // Lays out child elements from top to bottom with increasing y. + CLAY_TOP_TO_BOTTOM, +} Clay_LayoutDirection; + +// Controls the alignment along the x axis (horizontal) of child elements. +typedef CLAY_PACKED_ENUM { + // (Default) Aligns child elements to the left hand side of this element, offset by padding.width.left + CLAY_ALIGN_X_LEFT, + // Aligns child elements to the right hand side of this element, offset by padding.width.right + CLAY_ALIGN_X_RIGHT, + // Aligns child elements horizontally to the center of this element + CLAY_ALIGN_X_CENTER, +} Clay_LayoutAlignmentX; + +// Controls the alignment along the y axis (vertical) of child elements. +typedef CLAY_PACKED_ENUM { + // (Default) Aligns child elements to the top of this element, offset by padding.width.top + CLAY_ALIGN_Y_TOP, + // Aligns child elements to the bottom of this element, offset by padding.width.bottom + CLAY_ALIGN_Y_BOTTOM, + // Aligns child elements vertically to the center of this element + CLAY_ALIGN_Y_CENTER, +} Clay_LayoutAlignmentY; + +// Controls how the element takes up space inside its parent container. +typedef CLAY_PACKED_ENUM { + // (default) Wraps tightly to the size of the element's contents. + CLAY__SIZING_TYPE_FIT, + // Expands along this axis to fill available space in the parent element, sharing it with other GROW elements. + CLAY__SIZING_TYPE_GROW, + // Expects 0-1 range. Clamps the axis size to a percent of the parent container's axis size minus padding and child gaps. + CLAY__SIZING_TYPE_PERCENT, + // Clamps the axis size to an exact size in pixels. + CLAY__SIZING_TYPE_FIXED, +} Clay__SizingType; + +// Controls how child elements are aligned on each axis. +typedef struct Clay_ChildAlignment { + Clay_LayoutAlignmentX x; // Controls alignment of children along the x axis. + Clay_LayoutAlignmentY y; // Controls alignment of children along the y axis. +} Clay_ChildAlignment; + +// Controls the minimum and maximum size in pixels that this element is allowed to grow or shrink to, +// overriding sizing types such as FIT or GROW. +typedef struct Clay_SizingMinMax { + float min; // The smallest final size of the element on this axis will be this value in pixels. + float max; // The largest final size of the element on this axis will be this value in pixels. +} Clay_SizingMinMax; + +// Controls the sizing of this element along one axis inside its parent container. +typedef struct Clay_SizingAxis { + union { + Clay_SizingMinMax minMax; // Controls the minimum and maximum size in pixels that this element is allowed to grow or shrink to, overriding sizing types such as FIT or GROW. + float percent; // Expects 0-1 range. Clamps the axis size to a percent of the parent container's axis size minus padding and child gaps. + } size; + Clay__SizingType type; // Controls how the element takes up space inside its parent container. +} Clay_SizingAxis; + +// Controls the sizing of this element along one axis inside its parent container. +typedef struct Clay_Sizing { + Clay_SizingAxis width; // Controls the width sizing of the element, along the x axis. + Clay_SizingAxis height; // Controls the height sizing of the element, along the y axis. +} Clay_Sizing; + +// Controls "padding" in pixels, which is a gap between the bounding box of this element and where its children +// will be placed. +typedef struct Clay_Padding { + uint16_t left; + uint16_t right; + uint16_t top; + uint16_t bottom; +} Clay_Padding; + +CLAY__WRAPPER_STRUCT(Clay_Padding); + +// Controls various settings that affect the size and position of an element, as well as the sizes and positions +// of any child elements. +typedef struct Clay_LayoutConfig { + Clay_Sizing sizing; // Controls the sizing of this element inside it's parent container, including FIT, GROW, PERCENT and FIXED sizing. + Clay_Padding padding; // Controls "padding" in pixels, which is a gap between the bounding box of this element and where its children will be placed. + uint16_t childGap; // Controls the gap in pixels between child elements along the layout axis (horizontal gap for LEFT_TO_RIGHT, vertical gap for TOP_TO_BOTTOM). + Clay_ChildAlignment childAlignment; // Controls how child elements are aligned on each axis. + Clay_LayoutDirection layoutDirection; // Controls the direction in which child elements will be automatically laid out. +} Clay_LayoutConfig; + +CLAY__WRAPPER_STRUCT(Clay_LayoutConfig); + +extern Clay_LayoutConfig CLAY_LAYOUT_DEFAULT; + +// Controls how text "wraps", that is how it is broken into multiple lines when there is insufficient horizontal space. +typedef CLAY_PACKED_ENUM { + // (default) breaks on whitespace characters. + CLAY_TEXT_WRAP_WORDS, + // Don't break on space characters, only on newlines. + CLAY_TEXT_WRAP_NEWLINES, + // Disable text wrapping entirely. + CLAY_TEXT_WRAP_NONE, +} Clay_TextElementConfigWrapMode; + +// Controls how wrapped lines of text are horizontally aligned within the outer text bounding box. +typedef CLAY_PACKED_ENUM { + // (default) Horizontally aligns wrapped lines of text to the left hand side of their bounding box. + CLAY_TEXT_ALIGN_LEFT, + // Horizontally aligns wrapped lines of text to the center of their bounding box. + CLAY_TEXT_ALIGN_CENTER, + // Horizontally aligns wrapped lines of text to the right hand side of their bounding box. + CLAY_TEXT_ALIGN_RIGHT, +} Clay_TextAlignment; + +// Controls various functionality related to text elements. +typedef struct Clay_TextElementConfig { + // A pointer that will be transparently passed through to the resulting render command. + void *userData; + // The RGBA color of the font to render, conventionally specified as 0-255. + Clay_Color textColor; + // An integer transparently passed to Clay_MeasureText to identify the font to use. + // The debug view will pass fontId = 0 for its internal text. + uint16_t fontId; + // Controls the size of the font. Handled by the function provided to Clay_MeasureText. + uint16_t fontSize; + // Controls extra horizontal spacing between characters. Handled by the function provided to Clay_MeasureText. + uint16_t letterSpacing; + // Controls additional vertical space between wrapped lines of text. + uint16_t lineHeight; + // Controls how text "wraps", that is how it is broken into multiple lines when there is insufficient horizontal space. + // CLAY_TEXT_WRAP_WORDS (default) breaks on whitespace characters. + // CLAY_TEXT_WRAP_NEWLINES doesn't break on space characters, only on newlines. + // CLAY_TEXT_WRAP_NONE disables wrapping entirely. + Clay_TextElementConfigWrapMode wrapMode; + // Controls how wrapped lines of text are horizontally aligned within the outer text bounding box. + // CLAY_TEXT_ALIGN_LEFT (default) - Horizontally aligns wrapped lines of text to the left hand side of their bounding box. + // CLAY_TEXT_ALIGN_CENTER - Horizontally aligns wrapped lines of text to the center of their bounding box. + // CLAY_TEXT_ALIGN_RIGHT - Horizontally aligns wrapped lines of text to the right hand side of their bounding box. + Clay_TextAlignment textAlignment; +} Clay_TextElementConfig; + +CLAY__WRAPPER_STRUCT(Clay_TextElementConfig); + +// Aspect Ratio -------------------------------- + +// Controls various settings related to aspect ratio scaling element. +typedef struct Clay_AspectRatioElementConfig { + float aspectRatio; // A float representing the target "Aspect ratio" for an element, which is its final width divided by its final height. +} Clay_AspectRatioElementConfig; + +CLAY__WRAPPER_STRUCT(Clay_AspectRatioElementConfig); + +// Image -------------------------------- + +// Controls various settings related to image elements. +typedef struct Clay_ImageElementConfig { + void* imageData; // A transparent pointer used to pass image data through to the renderer. +} Clay_ImageElementConfig; + +CLAY__WRAPPER_STRUCT(Clay_ImageElementConfig); + +// Floating ----------------------------- + +// Controls where a floating element is offset relative to its parent element. +// Note: see https://github.com/user-attachments/assets/b8c6dfaa-c1b1-41a4-be55-013473e4a6ce for a visual explanation. +typedef CLAY_PACKED_ENUM { + CLAY_ATTACH_POINT_LEFT_TOP, + CLAY_ATTACH_POINT_LEFT_CENTER, + CLAY_ATTACH_POINT_LEFT_BOTTOM, + CLAY_ATTACH_POINT_CENTER_TOP, + CLAY_ATTACH_POINT_CENTER_CENTER, + CLAY_ATTACH_POINT_CENTER_BOTTOM, + CLAY_ATTACH_POINT_RIGHT_TOP, + CLAY_ATTACH_POINT_RIGHT_CENTER, + CLAY_ATTACH_POINT_RIGHT_BOTTOM, +} Clay_FloatingAttachPointType; + +// Controls where a floating element is offset relative to its parent element. +typedef struct Clay_FloatingAttachPoints { + Clay_FloatingAttachPointType element; // Controls the origin point on a floating element that attaches to its parent. + Clay_FloatingAttachPointType parent; // Controls the origin point on the parent element that the floating element attaches to. +} Clay_FloatingAttachPoints; + +// Controls how mouse pointer events like hover and click are captured or passed through to elements underneath a floating element. +typedef CLAY_PACKED_ENUM { + // (default) "Capture" the pointer event and don't allow events like hover and click to pass through to elements underneath. + CLAY_POINTER_CAPTURE_MODE_CAPTURE, + // CLAY_POINTER_CAPTURE_MODE_PARENT, TODO pass pointer through to attached parent + // Transparently pass through pointer events like hover and click to elements underneath the floating element. + CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, +} Clay_PointerCaptureMode; + +// Controls which element a floating element is "attached" to (i.e. relative offset from). +typedef CLAY_PACKED_ENUM { + // (default) Disables floating for this element. + CLAY_ATTACH_TO_NONE, + // Attaches this floating element to its parent, positioned based on the .attachPoints and .offset fields. + CLAY_ATTACH_TO_PARENT, + // Attaches this floating element to an element with a specific ID, specified with the .parentId field. positioned based on the .attachPoints and .offset fields. + CLAY_ATTACH_TO_ELEMENT_WITH_ID, + // Attaches this floating element to the root of the layout, which combined with the .offset field provides functionality similar to "absolute positioning". + CLAY_ATTACH_TO_ROOT, +} Clay_FloatingAttachToElement; + +// Controls whether or not a floating element is clipped to the same clipping rectangle as the element it's attached to. +typedef CLAY_PACKED_ENUM { + // (default) - The floating element does not inherit clipping. + CLAY_CLIP_TO_NONE, + // The floating element is clipped to the same clipping rectangle as the element it's attached to. + CLAY_CLIP_TO_ATTACHED_PARENT +} Clay_FloatingClipToElement; + +// Controls various settings related to "floating" elements, which are elements that "float" above other elements, potentially overlapping their boundaries, +// and not affecting the layout of sibling or parent elements. +typedef struct Clay_FloatingElementConfig { + // Offsets this floating element by the provided x,y coordinates from its attachPoints. + Clay_Vector2 offset; + // Expands the boundaries of the outer floating element without affecting its children. + Clay_Dimensions expand; + // When used in conjunction with .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID, attaches this floating element to the element in the hierarchy with the provided ID. + // Hint: attach the ID to the other element with .id = CLAY_ID("yourId"), and specify the id the same way, with .parentId = CLAY_ID("yourId").id + uint32_t parentId; + // Controls the z index of this floating element and all its children. Floating elements are sorted in ascending z order before output. + // zIndex is also passed to the renderer for all elements contained within this floating element. + int16_t zIndex; + // Controls how mouse pointer events like hover and click are captured or passed through to elements underneath / behind a floating element. + // Enum is of the form CLAY_ATTACH_POINT_foo_bar. See Clay_FloatingAttachPoints for more details. + // Note: see for a visual explanation. + Clay_FloatingAttachPoints attachPoints; + // Controls how mouse pointer events like hover and click are captured or passed through to elements underneath a floating element. + // CLAY_POINTER_CAPTURE_MODE_CAPTURE (default) - "Capture" the pointer event and don't allow events like hover and click to pass through to elements underneath. + // CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH - Transparently pass through pointer events like hover and click to elements underneath the floating element. + Clay_PointerCaptureMode pointerCaptureMode; + // Controls which element a floating element is "attached" to (i.e. relative offset from). + // CLAY_ATTACH_TO_NONE (default) - Disables floating for this element. + // CLAY_ATTACH_TO_PARENT - Attaches this floating element to its parent, positioned based on the .attachPoints and .offset fields. + // CLAY_ATTACH_TO_ELEMENT_WITH_ID - Attaches this floating element to an element with a specific ID, specified with the .parentId field. positioned based on the .attachPoints and .offset fields. + // CLAY_ATTACH_TO_ROOT - Attaches this floating element to the root of the layout, which combined with the .offset field provides functionality similar to "absolute positioning". + Clay_FloatingAttachToElement attachTo; + // Controls whether or not a floating element is clipped to the same clipping rectangle as the element it's attached to. + // CLAY_CLIP_TO_NONE (default) - The floating element does not inherit clipping. + // CLAY_CLIP_TO_ATTACHED_PARENT - The floating element is clipped to the same clipping rectangle as the element it's attached to. + Clay_FloatingClipToElement clipTo; +} Clay_FloatingElementConfig; + +CLAY__WRAPPER_STRUCT(Clay_FloatingElementConfig); + +// Custom ----------------------------- + +// Controls various settings related to custom elements. +typedef struct Clay_CustomElementConfig { + // A transparent pointer through which you can pass custom data to the renderer. + // Generates CUSTOM render commands. + void* customData; +} Clay_CustomElementConfig; + +CLAY__WRAPPER_STRUCT(Clay_CustomElementConfig); + +// Scroll ----------------------------- + +// Controls the axis on which an element switches to "scrolling", which clips the contents and allows scrolling in that direction. +typedef struct Clay_ClipElementConfig { + bool horizontal; // Clip overflowing elements on the X axis. + bool vertical; // Clip overflowing elements on the Y axis. + Clay_Vector2 childOffset; // Offsets the x,y positions of all child elements. Used primarily for scrolling containers. +} Clay_ClipElementConfig; + +CLAY__WRAPPER_STRUCT(Clay_ClipElementConfig); + +// Border ----------------------------- + +// Controls the widths of individual element borders. +typedef struct Clay_BorderWidth { + uint16_t left; + uint16_t right; + uint16_t top; + uint16_t bottom; + // Creates borders between each child element, depending on the .layoutDirection. + // e.g. for LEFT_TO_RIGHT, borders will be vertical lines, and for TOP_TO_BOTTOM borders will be horizontal lines. + // .betweenChildren borders will result in individual RECTANGLE render commands being generated. + uint16_t betweenChildren; +} Clay_BorderWidth; + +// Controls settings related to element borders. +typedef struct Clay_BorderElementConfig { + Clay_Color color; // Controls the color of all borders with width > 0. Conventionally represented as 0-255, but interpretation is up to the renderer. + Clay_BorderWidth width; // Controls the widths of individual borders. At least one of these should be > 0 for a BORDER render command to be generated. +} Clay_BorderElementConfig; + +CLAY__WRAPPER_STRUCT(Clay_BorderElementConfig); + +typedef struct { + Clay_BoundingBox boundingBox; + Clay_Color backgroundColor; + Clay_Color overlayColor; + Clay_Color borderColor; + Clay_BorderWidth borderWidth; +} Clay_TransitionData; + +typedef enum { + CLAY_TRANSITION_STATE_IDLE, + CLAY_TRANSITION_STATE_ENTERING, + CLAY_TRANSITION_STATE_TRANSITIONING, + CLAY_TRANSITION_STATE_EXITING, +} Clay_TransitionState; + +typedef enum { + CLAY_TRANSITION_PROPERTY_NONE = 0, + CLAY_TRANSITION_PROPERTY_X = 1, + CLAY_TRANSITION_PROPERTY_Y = 2, + CLAY_TRANSITION_PROPERTY_POSITION = CLAY_TRANSITION_PROPERTY_X | CLAY_TRANSITION_PROPERTY_Y, + CLAY_TRANSITION_PROPERTY_WIDTH = 4, + CLAY_TRANSITION_PROPERTY_HEIGHT = 8, + CLAY_TRANSITION_PROPERTY_DIMENSIONS = CLAY_TRANSITION_PROPERTY_WIDTH | CLAY_TRANSITION_PROPERTY_HEIGHT, + CLAY_TRANSITION_PROPERTY_BOUNDING_BOX = CLAY_TRANSITION_PROPERTY_POSITION | CLAY_TRANSITION_PROPERTY_DIMENSIONS, + CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR = 16, + CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR = 32, + CLAY_TRANSITION_PROPERTY_CORNER_RADIUS = 64, + CLAY_TRANSITION_PROPERTY_BORDER_COLOR = 128, + CLAY_TRANSITION_PROPERTY_BORDER_WIDTH = 256, + CLAY_TRANSITION_PROPERTY_BORDER = CLAY_TRANSITION_PROPERTY_BORDER_COLOR | CLAY_TRANSITION_PROPERTY_BORDER_WIDTH +} Clay_TransitionProperty; + +typedef struct { + Clay_TransitionState transitionState; + Clay_TransitionData initial; + Clay_TransitionData *current; + Clay_TransitionData target; + float elapsedTime; + float duration; + Clay_TransitionProperty properties; +} Clay_TransitionCallbackArguments; + +typedef CLAY_PACKED_ENUM { + CLAY_TRANSITION_ENTER_SKIP_ON_FIRST_PARENT_FRAME, + CLAY_TRANSITION_ENTER_TRIGGER_ON_FIRST_PARENT_FRAME, +} Clay_TransitionEnterTriggerType; + +typedef CLAY_PACKED_ENUM { + CLAY_TRANSITION_EXIT_SKIP_WHEN_PARENT_EXITS, + CLAY_TRANSITION_EXIT_TRIGGER_WHEN_PARENT_EXITS, +} Clay_TransitionExitTriggerType; + +typedef CLAY_PACKED_ENUM { + CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION, + CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION, +} Clay_TransitionInteractionHandlingType; + +typedef CLAY_PACKED_ENUM { + CLAY_EXIT_TRANSITION_ORDERING_UNDERNEATH_SIBLINGS, + CLAY_EXIT_TRANSITION_ORDERING_NATURAL_ORDER, + CLAY_EXIT_TRANSITION_ORDERING_ABOVE_SIBLINGS, +} Clay_ExitTransitionSiblingOrdering; + +// Controls settings related to transitions +typedef struct Clay_TransitionElementConfig { + bool (*handler)(Clay_TransitionCallbackArguments arguments); + float duration; + Clay_TransitionProperty properties; + Clay_TransitionInteractionHandlingType interactionHandling; + struct { + Clay_TransitionData (*setInitialState)(Clay_TransitionData targetState, Clay_TransitionProperty properties); + Clay_TransitionEnterTriggerType trigger; + } enter; + struct { + Clay_TransitionData (*setFinalState)(Clay_TransitionData initialState, Clay_TransitionProperty properties); + Clay_TransitionExitTriggerType trigger; + Clay_ExitTransitionSiblingOrdering siblingOrdering; + } exit; +} Clay_TransitionElementConfig; + +CLAY__WRAPPER_STRUCT(Clay_TransitionElementConfig); + +// Render Command Data ----------------------------- + +// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_TEXT +typedef struct Clay_TextRenderData { + // A string slice containing the text to be rendered. + // Note: this is not guaranteed to be null terminated. + Clay_StringSlice stringContents; + // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer. + Clay_Color textColor; + // An integer representing the font to use to render this text, transparently passed through from the text declaration. + uint16_t fontId; + uint16_t fontSize; + // Specifies the extra whitespace gap in pixels between each character. + uint16_t letterSpacing; + // The height of the bounding box for this line of text. + uint16_t lineHeight; +} Clay_TextRenderData; + +// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_RECTANGLE +typedef struct Clay_RectangleRenderData { + // The solid background color to fill this rectangle with. Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer. + Clay_Color backgroundColor; + // Controls the "radius", or corner rounding of elements, including rectangles, borders and images. + // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels. + Clay_CornerRadius cornerRadius; +} Clay_RectangleRenderData; + +// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_IMAGE +typedef struct Clay_ImageRenderData { + // The tint color for this image. Note that the default value is 0,0,0,0 and should likely be interpreted + // as "untinted". + // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer. + Clay_Color backgroundColor; + // Controls the "radius", or corner rounding of this image. + // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels. + Clay_CornerRadius cornerRadius; + // A pointer transparently passed through from the original element definition, typically used to represent image data. + void* imageData; +} Clay_ImageRenderData; + +// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_CUSTOM +typedef struct Clay_CustomRenderData { + // Passed through from .backgroundColor in the original element declaration. + // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer. + Clay_Color backgroundColor; + // Controls the "radius", or corner rounding of this custom element. + // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels. + Clay_CornerRadius cornerRadius; + // A pointer transparently passed through from the original element definition. + void* customData; +} Clay_CustomRenderData; + +// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_START || commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_END +typedef struct Clay_ClipRenderData { + bool horizontal; + bool vertical; +} Clay_ClipRenderData; + +// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START || commandType == CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END +typedef struct Clay_OverlayColorRenderData { + Clay_Color color; +} Clay_OverlayColorRenderData; + +// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_BORDER +typedef struct Clay_BorderRenderData { + // Controls a shared color for all this element's borders. + // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer. + Clay_Color color; + // Specifies the "radius", or corner rounding of this border element. + // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels. + Clay_CornerRadius cornerRadius; + // Controls individual border side widths. + Clay_BorderWidth width; +} Clay_BorderRenderData; + +// A struct union containing data specific to this command's .commandType +typedef union Clay_RenderData { + // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_RECTANGLE + Clay_RectangleRenderData rectangle; + // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_TEXT + Clay_TextRenderData text; + // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_IMAGE + Clay_ImageRenderData image; + // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_CUSTOM + Clay_CustomRenderData custom; + // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_BORDER + Clay_BorderRenderData border; + // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_START|END + Clay_ClipRenderData clip; + // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START|END + Clay_OverlayColorRenderData overlayColor; +} Clay_RenderData; + +// Miscellaneous Structs & Enums --------------------------------- + +// Data representing the current internal state of a scrolling element. +typedef struct Clay_ScrollContainerData { + // Note: This is a pointer to the real internal scroll position, mutating it may cause a change in final layout. + // Intended for use with external functionality that modifies scroll position, such as scroll bars or auto scrolling. + Clay_Vector2 *scrollPosition; + // The bounding box of the scroll element. + Clay_Dimensions scrollContainerDimensions; + // The outer dimensions of the inner scroll container content, including the padding of the parent scroll container. + Clay_Dimensions contentDimensions; + // The config that was originally passed to the clip element. + Clay_ClipElementConfig config; + // Indicates whether an actual scroll container matched the provided ID or if the default struct was returned. + bool found; +} Clay_ScrollContainerData; + +// Bounding box and other data for a specific UI element. +typedef struct Clay_ElementData { + // The rectangle that encloses this UI element, with the position relative to the root of the layout. + Clay_BoundingBox boundingBox; + // Indicates whether an actual Element matched the provided ID or if the default struct was returned. + bool found; +} Clay_ElementData; + +// Used by renderers to determine specific handling for each render command. +typedef CLAY_PACKED_ENUM { + // This command type should be skipped. + CLAY_RENDER_COMMAND_TYPE_NONE, + // The renderer should draw a solid color rectangle. + CLAY_RENDER_COMMAND_TYPE_RECTANGLE, + // The renderer should draw a colored border inset into the bounding box. + CLAY_RENDER_COMMAND_TYPE_BORDER, + // The renderer should draw text. + CLAY_RENDER_COMMAND_TYPE_TEXT, + // The renderer should draw an image. + CLAY_RENDER_COMMAND_TYPE_IMAGE, + // The renderer should begin clipping all future draw commands, only rendering content that falls within the provided boundingBox. + CLAY_RENDER_COMMAND_TYPE_SCISSOR_START, + // The renderer should finish any previously active clipping, and begin rendering elements in full again. + CLAY_RENDER_COMMAND_TYPE_SCISSOR_END, + // The renderer should begin performing a "color overlay" on all subsequent render commands until disabled again. + CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START, + // The renderer should disable any previously active "color overlay" and render elements with their standard colors again. + CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END, + // The renderer should provide a custom implementation for handling this render command based on its .customData + CLAY_RENDER_COMMAND_TYPE_CUSTOM, +} Clay_RenderCommandType; + +typedef struct Clay_RenderCommand { + // A rectangular box that fully encloses this UI element, with the position relative to the root of the layout. + Clay_BoundingBox boundingBox; + // A struct union containing data specific to this command's commandType. + Clay_RenderData renderData; + // A pointer transparently passed through from the original element declaration. + void *userData; + // The id of this element, transparently passed through from the original element declaration. + uint32_t id; + // The z order required for drawing this command correctly. + // Note: the render command array is already sorted in ascending order, and will produce correct results if drawn in naive order. + // This field is intended for use in batching renderers for improved performance. + int16_t zIndex; + // Specifies how to handle rendering of this command. + // CLAY_RENDER_COMMAND_TYPE_RECTANGLE - The renderer should draw a solid color rectangle. + // CLAY_RENDER_COMMAND_TYPE_BORDER - The renderer should draw a colored border inset into the bounding box. + // CLAY_RENDER_COMMAND_TYPE_TEXT - The renderer should draw text. + // CLAY_RENDER_COMMAND_TYPE_IMAGE - The renderer should draw an image. + // CLAY_RENDER_COMMAND_TYPE_SCISSOR_START - The renderer should begin clipping all future draw commands, only rendering content that falls within the provided boundingBox. + // CLAY_RENDER_COMMAND_TYPE_SCISSOR_END - The renderer should finish any previously active clipping, and begin rendering elements in full again. + // CLAY_RENDER_COMMAND_TYPE_CUSTOM - The renderer should provide a custom implementation for handling this render command based on its .customData + Clay_RenderCommandType commandType; +} Clay_RenderCommand; + +// A sized array of render commands. +typedef struct Clay_RenderCommandArray { + // The underlying max capacity of the array, not necessarily all initialized. + int32_t capacity; + // The number of initialized elements in this array. Used for loops and iteration. + int32_t length; + // A pointer to the first element in the internal array. + Clay_RenderCommand* internalArray; +} Clay_RenderCommandArray; + +// Represents the current state of interaction with clay this frame. +typedef CLAY_PACKED_ENUM { + // A left mouse click, or touch occurred this frame. + CLAY_POINTER_DATA_PRESSED_THIS_FRAME, + // The left mouse button click or touch happened at some point in the past, and is still currently held down this frame. + CLAY_POINTER_DATA_PRESSED, + // The left mouse button click or touch was released this frame. + CLAY_POINTER_DATA_RELEASED_THIS_FRAME, + // The left mouse button click or touch is not currently down / was released at some point in the past. + CLAY_POINTER_DATA_RELEASED, +} Clay_PointerDataInteractionState; + +// Information on the current state of pointer interactions this frame. +typedef struct Clay_PointerData { + // The position of the mouse / touch / pointer relative to the root of the layout. + Clay_Vector2 position; + // Represents the current state of interaction with clay this frame. + // CLAY_POINTER_DATA_PRESSED_THIS_FRAME - A left mouse click, or touch occurred this frame. + // CLAY_POINTER_DATA_PRESSED - The left mouse button click or touch happened at some point in the past, and is still currently held down this frame. + // CLAY_POINTER_DATA_RELEASED_THIS_FRAME - The left mouse button click or touch was released this frame. + // CLAY_POINTER_DATA_RELEASED - The left mouse button click or touch is not currently down / was released at some point in the past. + Clay_PointerDataInteractionState state; +} Clay_PointerData; + +typedef struct Clay_ElementDeclaration { + // Controls various settings that affect the size and position of an element, as well as the sizes and positions of any child elements. + Clay_LayoutConfig layout; + // Controls the background color of the resulting element. + // By convention specified as 0-255, but interpretation is up to the renderer. + // If no other config is specified, .backgroundColor will generate a RECTANGLE render command, otherwise it will be passed as a property to IMAGE or CUSTOM render commands. + Clay_Color backgroundColor; + // Perform an image editing style "Color Overlay" on this element and all its children, equivalent to + // glsl mix(elementColor, overlayColor.rgb, overlayColor.a) + Clay_Color overlayColor; + // Controls the "radius", or corner rounding of elements, including rectangles, borders and images. + Clay_CornerRadius cornerRadius; + // Controls settings related to aspect ratio scaling. + Clay_AspectRatioElementConfig aspectRatio; + // Controls settings related to image elements. + Clay_ImageElementConfig image; + // Controls whether and how an element "floats", which means it layers over the top of other elements in z order, and doesn't affect the position and size of siblings or parent elements. + // Note: in order to activate floating, .floating.attachTo must be set to something other than the default value. + Clay_FloatingElementConfig floating; + // Used to create CUSTOM render commands, usually to render element types not supported by Clay. + Clay_CustomElementConfig custom; + // Controls whether an element should clip its contents, as well as providing child x,y offset configuration for scrolling. + Clay_ClipElementConfig clip; + // Controls settings related to element borders, and will generate BORDER render commands. + Clay_BorderElementConfig border; + Clay_TransitionElementConfig transition; + // A pointer that will be transparently passed through to resulting render commands. + void *userData; +} Clay_ElementDeclaration; + +CLAY__WRAPPER_STRUCT(Clay_ElementDeclaration); + +// Represents the type of error clay encountered while computing layout. +typedef CLAY_PACKED_ENUM { + // A text measurement function wasn't provided using Clay_SetMeasureTextFunction(), or the provided function was null. + CLAY_ERROR_TYPE_TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED, + // Clay attempted to allocate its internal data structures but ran out of space. + // The arena passed to Clay_Initialize was created with a capacity smaller than that required by Clay_MinMemorySize(). + CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED, + // Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxElementCount(). + CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED, + // Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxMeasureTextCacheWordCount(). + CLAY_ERROR_TYPE_TEXT_MEASUREMENT_CAPACITY_EXCEEDED, + // Two elements were declared with exactly the same ID within one layout. + CLAY_ERROR_TYPE_DUPLICATE_ID, + // A floating element was declared using CLAY_ATTACH_TO_ELEMENT_ID and either an invalid .parentId was provided or no element with the provided .parentId was found. + CLAY_ERROR_TYPE_FLOATING_CONTAINER_PARENT_NOT_FOUND, + // An element was declared that using CLAY_SIZING_PERCENT but the percentage value was over 1. Percentage values are expected to be in the 0-1 range. + CLAY_ERROR_TYPE_PERCENTAGE_OVER_1, + // Clay encountered an internal error. It would be wonderful if you could report this so we can fix it! + CLAY_ERROR_TYPE_INTERNAL_ERROR, + // Clay__OpenElement was called more times than Clay__CloseElement, so there were still remaining open elements when the layout ended. + CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE, + CLAY_ERROR_TYPE_HASH_MAP_CAPACITY_EXCEEDED +} Clay_ErrorType; + +// Data to identify the error that clay has encountered. +typedef struct Clay_ErrorData { + // Represents the type of error clay encountered while computing layout. + // CLAY_ERROR_TYPE_TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED - A text measurement function wasn't provided using Clay_SetMeasureTextFunction(), or the provided function was null. + // CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED - Clay attempted to allocate its internal data structures but ran out of space. The arena passed to Clay_Initialize was created with a capacity smaller than that required by Clay_MinMemorySize(). + // CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED - Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxElementCount(). + // CLAY_ERROR_TYPE_TEXT_MEASUREMENT_CAPACITY_EXCEEDED - Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxMeasureTextCacheWordCount(). + // CLAY_ERROR_TYPE_DUPLICATE_ID - Two elements were declared with exactly the same ID within one layout. + // CLAY_ERROR_TYPE_FLOATING_CONTAINER_PARENT_NOT_FOUND - A floating element was declared using CLAY_ATTACH_TO_ELEMENT_ID and either an invalid .parentId was provided or no element with the provided .parentId was found. + // CLAY_ERROR_TYPE_PERCENTAGE_OVER_1 - An element was declared that using CLAY_SIZING_PERCENT but the percentage value was over 1. Percentage values are expected to be in the 0-1 range. + // CLAY_ERROR_TYPE_INTERNAL_ERROR - Clay encountered an internal error. It would be wonderful if you could report this so we can fix it! + // CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE - Clay__OpenElement was called more times than Clay__CloseElement, so there were still remaining open elements when the layout ended. + // CLAY_ERROR_TYPE_HASH_MAP_CAPACITY_EXCEEDED - Clay ran out of capacity in its internal hash map for storing element IDs -> elements. This limit can be increased with Clay_SetMaxElementCount(). + Clay_ErrorType errorType; + // A string containing human-readable error text that explains the error in more detail. + Clay_String errorText; + // A transparent pointer passed through from when the error handler was first provided. + void *userData; +} Clay_ErrorData; + +// A wrapper struct around Clay's error handler function. +typedef struct { + // A user provided function to call when Clay encounters an error during layout. + void (*errorHandlerFunction)(Clay_ErrorData errorText); + // A pointer that will be transparently passed through to the error handler when it is called. + void *userData; +} Clay_ErrorHandler; + +// Function Forward Declarations --------------------------------- + +// Public API functions ------------------------------------------ + +// Returns the size, in bytes, of the minimum amount of memory Clay requires to operate at its current settings. +CLAY_DLL_EXPORT uint32_t Clay_MinMemorySize(void); +// Creates an arena for clay to use for its internal allocations, given a certain capacity in bytes and a pointer to an allocation of at least that size. +// Intended to be used with Clay_MinMemorySize in the following way: +// uint32_t minMemoryRequired = Clay_MinMemorySize(); +// Clay_Arena clayMemory = Clay_CreateArenaWithCapacityAndMemory(minMemoryRequired, malloc(minMemoryRequired)); +CLAY_DLL_EXPORT Clay_Arena Clay_CreateArenaWithCapacityAndMemory(size_t capacity, void *memory); +// Sets the state of the "pointer" (i.e. the mouse or touch) in Clay's internal data. Used for detecting and responding to mouse events in the debug view, +// as well as for Clay_Hovered() and scroll element handling. +CLAY_DLL_EXPORT void Clay_SetPointerState(Clay_Vector2 position, bool pointerDown); +// Returns the state of the "pointer" (i.e. the mouse or touch) which was set via Clay_SetPointerState(). +CLAY_DLL_EXPORT Clay_PointerData Clay_GetPointerState(void); +// Initialize Clay's internal arena and setup required data before layout can begin. Only needs to be called once. +// - arena can be created using Clay_CreateArenaWithCapacityAndMemory() +// - layoutDimensions are the initial bounding dimensions of the layout (i.e. the screen width and height for a full screen layout) +// - errorHandler is used by Clay to inform you if something has gone wrong in configuration or layout. +CLAY_DLL_EXPORT Clay_Context* Clay_Initialize(Clay_Arena arena, Clay_Dimensions layoutDimensions, Clay_ErrorHandler errorHandler); +// Returns the Context that clay is currently using. Used when using multiple instances of clay simultaneously. +CLAY_DLL_EXPORT Clay_Context* Clay_GetCurrentContext(void); +// Sets the context that clay will use to compute the layout. +// Used to restore a context saved from Clay_GetCurrentContext when using multiple instances of clay simultaneously. +CLAY_DLL_EXPORT void Clay_SetCurrentContext(Clay_Context* context); +// Updates the state of Clay's internal scroll data, updating scroll content positions if scrollDelta is non zero, and progressing momentum scrolling. +// - enableDragScrolling when set to true will enable mobile device like "touch drag" scroll of scroll containers, including momentum scrolling after the touch has ended. +// - scrollDelta is the amount to scroll this frame on each axis in pixels. +// - deltaTime is the time in seconds since the last "frame" (scroll update) +CLAY_DLL_EXPORT void Clay_UpdateScrollContainers(bool enableDragScrolling, Clay_Vector2 scrollDelta, float deltaTime); +// Returns the internally stored scroll offset for the currently open element. +// Generally intended for use with clip elements to create scrolling containers. +CLAY_DLL_EXPORT Clay_Vector2 Clay_GetScrollOffset(void); +// Updates the layout dimensions in response to the window or outer container being resized. +CLAY_DLL_EXPORT void Clay_SetLayoutDimensions(Clay_Dimensions dimensions); +// Returns the current dimensions set by Clay_SetLayoutDimensions. +CLAY_DLL_EXPORT Clay_Dimensions Clay_GetLayoutDimensions(void); +// Called before starting any layout declarations. +CLAY_DLL_EXPORT void Clay_BeginLayout(void); +// Called when all layout declarations are finished. +// Computes the layout and generates and returns the array of render commands to draw. +CLAY_DLL_EXPORT Clay_RenderCommandArray Clay_EndLayout(float deltaTime); +// Gets the ID of the currently open element, useful for retrieving IDs generated by CLAY_AUTO_ID() +CLAY_DLL_EXPORT uint32_t Clay_GetOpenElementId(void); +// Calculates a hash ID from the given idString. +// Generally only used for dynamic strings when CLAY_ID("stringLiteral") can't be used. +CLAY_DLL_EXPORT Clay_ElementId Clay_GetElementId(Clay_String idString); +// Calculates a hash ID from the given idString and index. +// - index is used to avoid constructing dynamic ID strings in loops. +// Generally only used for dynamic strings when CLAY_IDI("stringLiteral", index) can't be used. +CLAY_DLL_EXPORT Clay_ElementId Clay_GetElementIdWithIndex(Clay_String idString, uint32_t index); +// Returns layout data such as the final calculated bounding box for an element with a given ID. +// The returned Clay_ElementData contains a `found` bool that will be true if an element with the provided ID was found. +// This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings. +CLAY_DLL_EXPORT Clay_ElementData Clay_GetElementData(Clay_ElementId id); +// Returns true if the pointer position provided by Clay_SetPointerState is within the current element's bounding box. +// Works during element declaration, e.g. CLAY({ .backgroundColor = Clay_Hovered() ? BLUE : RED }); +CLAY_DLL_EXPORT bool Clay_Hovered(void); +// Bind a callback that will be called when the pointer position provided by Clay_SetPointerState is within the current element's bounding box. +// - onHoverFunction is a function pointer to a user defined function. +// - userData is a pointer that will be transparently passed through when the onHoverFunction is called. +CLAY_DLL_EXPORT void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerData, void *userData), void *userData); +// An imperative function that returns true if the pointer position provided by Clay_SetPointerState is within the element with the provided ID's bounding box. +// This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings. +CLAY_DLL_EXPORT bool Clay_PointerOver(Clay_ElementId elementId); +// Returns the array of element IDs that the pointer is currently over. +CLAY_DLL_EXPORT Clay_ElementIdArray Clay_GetPointerOverIds(void); +// Returns data representing the state of the scrolling element with the provided ID. +// The returned Clay_ScrollContainerData contains a `found` bool that will be true if a scroll element was found with the provided ID. +// An imperative function that returns true if the pointer position provided by Clay_SetPointerState is within the element with the provided ID's bounding box. +// This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings. +CLAY_DLL_EXPORT Clay_ScrollContainerData Clay_GetScrollContainerData(Clay_ElementId id); +// Binds a callback function that Clay will call to determine the dimensions of a given string slice. +// - measureTextFunction is a user provided function that adheres to the interface Clay_Dimensions (Clay_StringSlice text, Clay_TextElementConfig *config, void *userData); +// - userData is a pointer that will be transparently passed through when the measureTextFunction is called. +CLAY_DLL_EXPORT void Clay_SetMeasureTextFunction(Clay_Dimensions (*measureTextFunction)(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData), void *userData); +// Experimental - Used in cases where Clay needs to integrate with a system that manages its own scrolling containers externally. +// Please reach out if you plan to use this function, as it may be subject to change. +CLAY_DLL_EXPORT void Clay_SetQueryScrollOffsetFunction(Clay_Vector2 (*queryScrollOffsetFunction)(uint32_t elementId, void *userData), void *userData); +// A bounds-checked "get" function for the Clay_RenderCommandArray returned from Clay_EndLayout(). +CLAY_DLL_EXPORT Clay_RenderCommand * Clay_RenderCommandArray_Get(Clay_RenderCommandArray* array, int32_t index); +// Enables and disables Clay's internal debug tools. +// This state is retained and does not need to be set each frame. +CLAY_DLL_EXPORT void Clay_SetDebugModeEnabled(bool enabled); +// Returns true if Clay's internal debug tools are currently enabled. +CLAY_DLL_EXPORT bool Clay_IsDebugModeEnabled(void); +// Enables and disables visibility culling. By default, Clay will not generate render commands for elements whose bounding box is entirely outside the screen. +CLAY_DLL_EXPORT void Clay_SetCullingEnabled(bool enabled); +// Returns the maximum number of UI elements supported by Clay's current configuration. +CLAY_DLL_EXPORT int32_t Clay_GetMaxElementCount(void); +// Modifies the maximum number of UI elements supported by Clay's current configuration. +// This may require reallocating additional memory, and re-calling Clay_Initialize(); +CLAY_DLL_EXPORT void Clay_SetMaxElementCount(int32_t maxElementCount); +// Returns the maximum number of measured "words" (whitespace seperated runs of characters) that Clay can store in its internal text measurement cache. +CLAY_DLL_EXPORT int32_t Clay_GetMaxMeasureTextCacheWordCount(void); +// Modifies the maximum number of measured "words" (whitespace seperated runs of characters) that Clay can store in its internal text measurement cache. +// This may require reallocating additional memory, and re-calling Clay_Initialize(); +CLAY_DLL_EXPORT void Clay_SetMaxMeasureTextCacheWordCount(int32_t maxMeasureTextCacheWordCount); +// Resets Clay's internal text measurement cache. Useful if font mappings have changed or fonts have been reloaded. +CLAY_DLL_EXPORT void Clay_ResetMeasureTextCache(void); +// A built in transition function that uses the "Ease Out" curve +CLAY_DLL_EXPORT bool Clay_EaseOut(Clay_TransitionCallbackArguments arguments); + +// Internal API functions required by macros ---------------------- + +CLAY_DLL_EXPORT void Clay__OpenElement(void); +CLAY_DLL_EXPORT void Clay__OpenElementWithId(Clay_ElementId elementId); +CLAY_DLL_EXPORT void Clay__ConfigureOpenElement(const Clay_ElementDeclaration config); +CLAY_DLL_EXPORT void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *config); +CLAY_DLL_EXPORT void Clay__CloseElement(void); +CLAY_DLL_EXPORT Clay_ElementId Clay__HashString(Clay_String key, uint32_t seed); +CLAY_DLL_EXPORT Clay_ElementId Clay__HashStringWithOffset(Clay_String key, uint32_t offset, uint32_t seed); +CLAY_DLL_EXPORT void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig textConfig); + +extern Clay_Color Clay__debugViewHighlightColor; +extern uint32_t Clay__debugViewWidth; + +#ifdef __cplusplus +} +#endif + +#endif // CLAY_HEADER + +// ----------------------------------------- +// IMPLEMENTATION -------------------------- +// ----------------------------------------- +#ifdef CLAY_IMPLEMENTATION +#undef CLAY_IMPLEMENTATION + +#ifndef CLAY__NULL +#define CLAY__NULL 0 +#endif + +#ifndef CLAY__MAXFLOAT +#define CLAY__MAXFLOAT 3.40282346638528859812e+38F +#endif + +Clay_LayoutConfig CLAY_LAYOUT_DEFAULT = CLAY__DEFAULT_STRUCT; + +Clay_Color Clay__Color_DEFAULT = CLAY__DEFAULT_STRUCT; +Clay_CornerRadius Clay__CornerRadius_DEFAULT = CLAY__DEFAULT_STRUCT; +Clay_BorderWidth Clay__BorderWidth_DEFAULT = CLAY__DEFAULT_STRUCT; + +// The below functions define array bounds checking and convenience functions for a provided type. +#define CLAY__ARRAY_DEFINE_FUNCTIONS(typeName, arrayName) \ + \ +typedef struct \ +{ \ + int32_t length; \ + typeName *internalArray; \ +} arrayName##Slice; \ + \ +typeName typeName##_DEFAULT = CLAY__DEFAULT_STRUCT; \ + \ +arrayName arrayName##_Allocate_Arena(int32_t capacity, Clay_Arena *arena) { \ + return CLAY__INIT(arrayName){.capacity = capacity, .length = 0, \ + .internalArray = (typeName *)Clay__Array_Allocate_Arena(capacity, sizeof(typeName), arena)}; \ +} \ + \ +typeName *arrayName##_Get(arrayName *array, int32_t index) { \ + return Clay__Array_RangeCheck(index, array->length) ? &array->internalArray[index] : &typeName##_DEFAULT; \ +} \ + \ +typeName arrayName##_GetValue(arrayName *array, int32_t index) { \ + return Clay__Array_RangeCheck(index, array->length) ? array->internalArray[index] : typeName##_DEFAULT; \ +} \ + \ +typeName *arrayName##_GetCheckCapacity(arrayName *array, int32_t index) { \ + return Clay__Array_RangeCheck(index, array->capacity) ? &array->internalArray[index] : &typeName##_DEFAULT; \ +} \ + \ +typeName *arrayName##_Add(arrayName *array, typeName item) { \ + if (Clay__Array_AddCapacityCheck(array->length, array->capacity)) { \ + array->internalArray[array->length++] = item; \ + return &array->internalArray[array->length - 1]; \ + } \ + return &typeName##_DEFAULT; \ +} \ + \ +typeName *arrayName##Slice_Get(arrayName##Slice *slice, int32_t index) { \ + return Clay__Array_RangeCheck(index, slice->length) ? &slice->internalArray[index] : &typeName##_DEFAULT; \ +} \ + \ +typeName arrayName##_RemoveSwapback(arrayName *array, int32_t index) { \ + if (Clay__Array_RangeCheck(index, array->length)) { \ + array->length--; \ + typeName removed = array->internalArray[index]; \ + array->internalArray[index] = array->internalArray[array->length]; \ + return removed; \ + } \ + return typeName##_DEFAULT; \ +} \ + \ +typeName* arrayName##_Set(arrayName *array, int32_t index, typeName value) { \ + if (Clay__Array_RangeCheck(index, array->capacity)) { \ + array->internalArray[index] = value; \ + array->length = index < array->length ? array->length : index + 1; \ + return &array->internalArray[index];\ + } \ + return NULL;\ +} \ + \ +typeName* arrayName##_Set_DontTouchLength(arrayName *array, int32_t index, typeName value) { \ + if (Clay__Array_RangeCheck(index, array->capacity)) { \ + array->internalArray[index] = value; \ + return &array->internalArray[index];\ + } \ + return NULL;\ +} \ + +#define CLAY__ARRAY_DEFINE(typeName, arrayName) \ +typedef struct \ +{ \ + int32_t capacity; \ + int32_t length; \ + typeName *internalArray; \ +} arrayName; \ + \ +CLAY__ARRAY_DEFINE_FUNCTIONS(typeName, arrayName) \ + +Clay_Context *Clay__currentContext; +int32_t Clay__defaultMaxElementCount = 8192; +int32_t Clay__defaultMaxMeasureTextWordCacheCount = 16384; + +void Clay__ErrorHandlerFunctionDefault(Clay_ErrorData errorText) { + (void) errorText; +} + +Clay_String CLAY__SPACECHAR = { .length = 1, .chars = " " }; +Clay_String CLAY__STRING_DEFAULT = { .length = 0, .chars = NULL }; + +typedef struct { + bool maxElementsExceeded; + bool maxRenderCommandsExceeded; + bool maxTextMeasureCacheExceeded; + bool textMeasurementFunctionNotSet; + bool hashMapCapacityExceeded; +} Clay_BooleanWarnings; + +typedef struct { + Clay_String baseMessage; + Clay_String dynamicMessage; +} Clay__Warning; + +Clay__Warning CLAY__WARNING_DEFAULT = CLAY__DEFAULT_STRUCT; + +typedef struct { + int32_t capacity; + int32_t length; + Clay__Warning *internalArray; +} Clay__WarningArray; + +Clay__WarningArray Clay__WarningArray_Allocate_Arena(int32_t capacity, Clay_Arena *arena); +Clay__Warning *Clay__WarningArray_Add(Clay__WarningArray *array, Clay__Warning item); +void* Clay__Array_Allocate_Arena(int32_t capacity, uint32_t itemSize, Clay_Arena *arena); +bool Clay__Array_RangeCheck(int32_t index, int32_t length); +bool Clay__Array_AddCapacityCheck(int32_t length, int32_t capacity); + +CLAY__ARRAY_DEFINE(bool, Clay__boolArray) +CLAY__ARRAY_DEFINE(int32_t, Clay__int32_tArray) +CLAY__ARRAY_DEFINE(char, Clay__charArray) +CLAY__ARRAY_DEFINE_FUNCTIONS(Clay_ElementId, Clay_ElementIdArray) +CLAY__ARRAY_DEFINE(Clay_String, Clay__StringArray) +CLAY__ARRAY_DEFINE_FUNCTIONS(Clay_RenderCommand, Clay_RenderCommandArray) + +typedef struct { + Clay_Dimensions dimensions; + Clay_String line; +} Clay__WrappedTextLine; + +CLAY__ARRAY_DEFINE(Clay__WrappedTextLine, Clay__WrappedTextLineArray) + +typedef struct { + Clay_String text; + Clay_Dimensions preferredDimensions; + Clay__WrappedTextLineArraySlice wrappedLines; +} Clay__TextElementData; + +typedef struct { + int32_t *elements; + uint16_t length; +} Clay__LayoutElementChildren; + +typedef struct Clay_LayoutElement { + Clay__LayoutElementChildren children; + Clay_Dimensions dimensions; + Clay_Dimensions minDimensions; + union { + Clay_ElementDeclaration config; + struct { + Clay_TextElementConfig textConfig; + Clay__TextElementData textElementData; + }; + }; + uint32_t id; + uint16_t floatingChildrenCount; + bool isTextElement; + // True if the element is currently in an exit transition, and is "synthetic" + // i.e. data was retained from previous frames + bool exiting; +} Clay_LayoutElement; + +CLAY__ARRAY_DEFINE(Clay_LayoutElement, Clay_LayoutElementArray) + +typedef struct { + Clay_LayoutElement *layoutElement; + Clay_BoundingBox boundingBox; + Clay_Dimensions contentSize; + Clay_Vector2 scrollOrigin; + Clay_Vector2 pointerOrigin; + Clay_Vector2 scrollMomentum; + Clay_Vector2 scrollPosition; + Clay_Vector2 previousDelta; + float momentumTime; + uint32_t elementId; + bool openThisFrame; + bool pointerScrollActive; +} Clay__ScrollContainerDataInternal; + +CLAY__ARRAY_DEFINE(Clay__ScrollContainerDataInternal, Clay__ScrollContainerDataInternalArray) + +// Data representing the current internal state of a transition element. +typedef struct Clay__TransitionDataInternal { + Clay_TransitionData initialState; + Clay_TransitionData currentState; + Clay_TransitionData targetState; + Clay_LayoutElement* elementThisFrame; + Clay_Vector2 oldParentRelativePosition; + uint32_t elementId; + uint32_t parentId; + uint32_t siblingIndex; + float elapsedTime; + Clay_TransitionState state; + bool transitionOut; + bool reparented; + Clay_TransitionProperty activeProperties; +} Clay__TransitionDataInternal; + +CLAY__ARRAY_DEFINE(Clay__TransitionDataInternal, Clay__TransitionDataInternalArray) + +typedef struct { // todo get this struct into a single cache line + Clay_BoundingBox boundingBox; + Clay_ElementId elementId; + Clay_LayoutElement* layoutElement; + void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData); + void *hoverFunctionUserData; + int32_t nextIndex; + uint32_t generation; + bool appearedThisFrame; + struct { + bool collision; + bool collapsed; + } debugData; +} Clay_LayoutElementHashMapItem; + +CLAY__ARRAY_DEFINE(Clay_LayoutElementHashMapItem, Clay__LayoutElementHashMapItemArray) + +typedef struct { + int32_t startOffset; + int32_t length; + float width; + int32_t next; +} Clay__MeasuredWord; + +CLAY__ARRAY_DEFINE(Clay__MeasuredWord, Clay__MeasuredWordArray) + +typedef struct { + Clay_Dimensions unwrappedDimensions; + int32_t measuredWordsStartIndex; + float minWidth; + bool containsNewlines; + // Hash map data + uint32_t id; + int32_t nextIndex; + uint32_t generation; +} Clay__MeasureTextCacheItem; + +CLAY__ARRAY_DEFINE(Clay__MeasureTextCacheItem, Clay__MeasureTextCacheItemArray) + +typedef struct { + Clay_LayoutElement *layoutElement; + Clay_Vector2 position; + Clay_Vector2 nextChildOffset; + bool parentMovedThisFramed; // Used to relativise transitions +} Clay__LayoutElementTreeNode; + +CLAY__ARRAY_DEFINE(Clay__LayoutElementTreeNode, Clay__LayoutElementTreeNodeArray) + +typedef struct { + int32_t layoutElementIndex; + uint32_t parentId; // This can be zero in the case of the root layout tree + uint32_t clipElementId; // This can be zero if there is no clip element + int16_t zIndex; + Clay_Vector2 pointerOffset; // Only used when scroll containers are managed externally +} Clay__LayoutElementTreeRoot; + +CLAY__ARRAY_DEFINE(Clay__LayoutElementTreeRoot, Clay__LayoutElementTreeRootArray) + +struct Clay_Context { + int32_t maxElementCount; + int32_t maxMeasureTextCacheWordCount; + int32_t exitingElementsLength; + int32_t exitingElementsChildrenLength; + bool warningsEnabled; + bool rootResizedLastFrame; + Clay_ErrorHandler errorHandler; + Clay_BooleanWarnings booleanWarnings; + Clay__WarningArray warnings; + + Clay_PointerData pointerInfo; + Clay_Dimensions layoutDimensions; + Clay_ElementId dynamicElementIndexBaseHash; + uint32_t dynamicElementIndex; + bool debugModeEnabled; + bool disableCulling; + bool externalScrollHandlingEnabled; + uint32_t debugSelectedElementId; + uint32_t generation; + uintptr_t arenaResetOffset; + void *measureTextUserData; + void *queryScrollOffsetUserData; + Clay_Arena internalArena; + // Layout Elements / Render Commands + Clay_LayoutElementArray layoutElements; + Clay_RenderCommandArray renderCommands; + Clay__int32_tArray openLayoutElementStack; + Clay__int32_tArray layoutElementChildren; + Clay__int32_tArray layoutElementChildrenBuffer; + Clay__int32_tArray reusableElementIndexBuffer; + Clay__int32_tArray layoutElementClipElementIds; + // Misc Data Structures + Clay__StringArray layoutElementIdStrings; + Clay__WrappedTextLineArray wrappedTextLines; + Clay__LayoutElementTreeNodeArray layoutElementTreeNodeArray1; + Clay__LayoutElementTreeRootArray layoutElementTreeRoots; + Clay__LayoutElementHashMapItemArray layoutElementsHashMapInternal; + Clay__int32_tArray layoutElementsHashMap; + Clay__int32_tArray layoutElementsHashMapFreeList; + Clay__MeasureTextCacheItemArray measureTextHashMapInternal; + Clay__int32_tArray measureTextHashMapInternalFreeList; + Clay__int32_tArray measureTextHashMap; + Clay__MeasuredWordArray measuredWords; + Clay__int32_tArray measuredWordsFreeList; + Clay__int32_tArray openClipElementStack; + Clay_ElementIdArray pointerOverIds; + Clay__ScrollContainerDataInternalArray scrollContainerDatas; + Clay__TransitionDataInternalArray transitionDatas; + Clay__boolArray treeNodeVisited; + Clay__charArray dynamicStringData; +}; + +Clay_Context* Clay__Context_Allocate_Arena(Clay_Arena *arena) { + size_t totalSizeBytes = sizeof(Clay_Context); + if (totalSizeBytes > arena->capacity) + { + return NULL; + } + arena->nextAllocation += totalSizeBytes; + return (Clay_Context*)(arena->memory); +} + +Clay_String Clay__WriteStringToCharBuffer(Clay__charArray *buffer, Clay_String string) { + for (int32_t i = 0; i < string.length; i++) { + buffer->internalArray[buffer->length + i] = string.chars[i]; + } + buffer->length += string.length; + return CLAY__INIT(Clay_String) { .length = string.length, .chars = (const char *)(buffer->internalArray + buffer->length - string.length) }; +} + +#ifdef CLAY_WASM + __attribute__((import_module("clay"), import_name("measureTextFunction"))) Clay_Dimensions Clay__MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData); + __attribute__((import_module("clay"), import_name("queryScrollOffsetFunction"))) Clay_Vector2 Clay__QueryScrollOffset(uint32_t elementId, void *userData); +#else + Clay_Dimensions (*Clay__MeasureText)(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData); + Clay_Vector2 (*Clay__QueryScrollOffset)(uint32_t elementId, void *userData); +#endif + +Clay_LayoutElement* Clay__GetOpenLayoutElement(void) { + Clay_Context* context = Clay_GetCurrentContext(); + return Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1)); +} + +Clay_LayoutElement* Clay__GetParentElement(void) { + Clay_Context* context = Clay_GetCurrentContext(); + return Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2)); +} + +uint32_t Clay__GetParentElementId(void) { + return Clay__GetParentElement()->id; +} + +bool Clay__BorderHasAnyWidth(Clay_BorderElementConfig* borderConfig) { + return borderConfig->width.betweenChildren > 0 || borderConfig->width.left > 0 || borderConfig->width.right > 0 || borderConfig->width.top > 0 || borderConfig->width.bottom > 0; +} + +Clay_ElementId Clay__HashNumber(const uint32_t offset, const uint32_t seed) { + uint32_t hash = seed; + hash += (offset + 48); + hash += (hash << 10); + hash ^= (hash >> 6); + + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = offset, .baseId = seed, .stringId = CLAY__STRING_DEFAULT }; // Reserve the hash result of zero as "null id" +} + +Clay_ElementId Clay__HashString(Clay_String key, const uint32_t seed) { + uint32_t hash = seed; + + for (int32_t i = 0; i < key.length; i++) { + hash += key.chars[i]; + hash += (hash << 10); + hash ^= (hash >> 6); + } + + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = 0, .baseId = hash + 1, .stringId = key }; // Reserve the hash result of zero as "null id" +} + +Clay_ElementId Clay__HashStringWithOffset(Clay_String key, const uint32_t offset, const uint32_t seed) { + uint32_t hash = 0; + uint32_t base = seed; + + for (int32_t i = 0; i < key.length; i++) { + base += key.chars[i]; + base += (base << 10); + base ^= (base >> 6); + } + hash = base; + hash += offset; + hash += (hash << 10); + hash ^= (hash >> 6); + + hash += (hash << 3); + base += (base << 3); + hash ^= (hash >> 11); + base ^= (base >> 11); + hash += (hash << 15); + base += (base << 15); + return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = offset, .baseId = base + 1, .stringId = key }; // Reserve the hash result of zero as "null id" +} + +#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64)) +static inline __m128i Clay__SIMDRotateLeft(__m128i x, int r) { + return _mm_or_si128(_mm_slli_epi64(x, r), _mm_srli_epi64(x, 64 - r)); +} + +static inline void Clay__SIMDARXMix(__m128i* a, __m128i* b) { + *a = _mm_add_epi64(*a, *b); + *b = _mm_xor_si128(Clay__SIMDRotateLeft(*b, 17), *a); +} + +uint64_t Clay__HashData(const uint8_t* data, size_t length) { + // Pinched these constants from the BLAKE implementation + __m128i v0 = _mm_set1_epi64x(0x6a09e667f3bcc908ULL); + __m128i v1 = _mm_set1_epi64x(0xbb67ae8584caa73bULL); + __m128i v2 = _mm_set1_epi64x(0x3c6ef372fe94f82bULL); + __m128i v3 = _mm_set1_epi64x(0xa54ff53a5f1d36f1ULL); + + uint8_t overflowBuffer[16] = { 0 }; // Temporary buffer for small inputs + + while (length > 0) { + __m128i msg; + if (length >= 16) { + msg = _mm_loadu_si128((const __m128i*)data); + data += 16; + length -= 16; + } + else { + for (size_t i = 0; i < length; i++) { + overflowBuffer[i] = data[i]; + } + msg = _mm_loadu_si128((const __m128i*)overflowBuffer); + length = 0; + } + + v0 = _mm_xor_si128(v0, msg); + Clay__SIMDARXMix(&v0, &v1); + Clay__SIMDARXMix(&v2, &v3); + + v0 = _mm_add_epi64(v0, v2); + v1 = _mm_add_epi64(v1, v3); + } + + Clay__SIMDARXMix(&v0, &v1); + Clay__SIMDARXMix(&v2, &v3); + v0 = _mm_add_epi64(v0, v2); + v1 = _mm_add_epi64(v1, v3); + v0 = _mm_add_epi64(v0, v1); + + uint64_t result[2]; + _mm_storeu_si128((__m128i*)result, v0); + + return result[0] ^ result[1]; +} +#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__) +static inline void Clay__SIMDARXMix(uint64x2_t* a, uint64x2_t* b) { + *a = vaddq_u64(*a, *b); + *b = veorq_u64(vorrq_u64(vshlq_n_u64(*b, 17), vshrq_n_u64(*b, 64 - 17)), *a); +} + +uint64_t Clay__HashData(const uint8_t* data, size_t length) { + // Pinched these constants from the BLAKE implementation + uint64x2_t v0 = vdupq_n_u64(0x6a09e667f3bcc908ULL); + uint64x2_t v1 = vdupq_n_u64(0xbb67ae8584caa73bULL); + uint64x2_t v2 = vdupq_n_u64(0x3c6ef372fe94f82bULL); + uint64x2_t v3 = vdupq_n_u64(0xa54ff53a5f1d36f1ULL); + + uint8_t overflowBuffer[8] = { 0 }; + + while (length > 0) { + uint64x2_t msg; + if (length > 16) { + msg = vld1q_u64((const uint64_t*)data); + data += 16; + length -= 16; + } + else if (length > 8) { + msg = vcombine_u64(vld1_u64((const uint64_t*)data), vdup_n_u64(0)); + data += 8; + length -= 8; + } + else { + for (size_t i = 0; i < length; i++) { + overflowBuffer[i] = data[i]; + } + uint8x8_t lower = vld1_u8(overflowBuffer); + msg = vreinterpretq_u64_u8(vcombine_u8(lower, vdup_n_u8(0))); + length = 0; + } + v0 = veorq_u64(v0, msg); + Clay__SIMDARXMix(&v0, &v1); + Clay__SIMDARXMix(&v2, &v3); + + v0 = vaddq_u64(v0, v2); + v1 = vaddq_u64(v1, v3); + } + + Clay__SIMDARXMix(&v0, &v1); + Clay__SIMDARXMix(&v2, &v3); + v0 = vaddq_u64(v0, v2); + v1 = vaddq_u64(v1, v3); + v0 = vaddq_u64(v0, v1); + + uint64_t result[2]; + vst1q_u64(result, v0); + + return result[0] ^ result[1]; +} +#else +uint64_t Clay__HashData(const uint8_t* data, size_t length) { + uint64_t hash = 0; + + for (size_t i = 0; i < length; i++) { + hash += data[i]; + hash += (hash << 10); + hash ^= (hash >> 6); + } + return hash; +} +#endif + +uint32_t Clay__HashStringContentsWithConfig(Clay_String *text, Clay_TextElementConfig *config) { + uint32_t hash = 0; + if (text->isStaticallyAllocated) { + hash += (uintptr_t)text->chars; + hash += (hash << 10); + hash ^= (hash >> 6); + hash += text->length; + hash += (hash << 10); + hash ^= (hash >> 6); + } else { + hash = Clay__HashData((const uint8_t *)text->chars, text->length) % UINT32_MAX; + } + + hash += config->fontId; + hash += (hash << 10); + hash ^= (hash >> 6); + + hash += config->fontSize; + hash += (hash << 10); + hash ^= (hash >> 6); + + hash += config->letterSpacing; + hash += (hash << 10); + hash ^= (hash >> 6); + + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return hash + 1; // Reserve the hash result of zero as "null id" +} + +Clay__MeasuredWord *Clay__AddMeasuredWord(Clay__MeasuredWord word, Clay__MeasuredWord *previousWord) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->measuredWordsFreeList.length > 0) { + uint32_t newItemIndex = Clay__int32_tArray_GetValue(&context->measuredWordsFreeList, (int)context->measuredWordsFreeList.length - 1); + context->measuredWordsFreeList.length--; + Clay__MeasuredWordArray_Set(&context->measuredWords, (int)newItemIndex, word); + previousWord->next = (int32_t)newItemIndex; + return Clay__MeasuredWordArray_Get(&context->measuredWords, (int)newItemIndex); + } else { + previousWord->next = (int32_t)context->measuredWords.length; + return Clay__MeasuredWordArray_Add(&context->measuredWords, word); + } +} + +Clay__MeasureTextCacheItem *Clay__MeasureTextCached(Clay_String *text, Clay_TextElementConfig *config) { + Clay_Context* context = Clay_GetCurrentContext(); + #ifndef CLAY_WASM + if (!Clay__MeasureText) { + if (!context->booleanWarnings.textMeasurementFunctionNotSet) { + context->booleanWarnings.textMeasurementFunctionNotSet = true; + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED, + .errorText = CLAY_STRING("Clay's internal MeasureText function is null. You may have forgotten to call Clay_SetMeasureTextFunction(), or passed a NULL function pointer by mistake."), + .userData = context->errorHandler.userData }); + } + return &Clay__MeasureTextCacheItem_DEFAULT; + } + #endif + uint32_t id = Clay__HashStringContentsWithConfig(text, config); + uint32_t hashBucket = id % (context->maxMeasureTextCacheWordCount / 32); + int32_t elementIndexPrevious = 0; + int32_t elementIndex = context->measureTextHashMap.internalArray[hashBucket]; + while (elementIndex != 0) { + Clay__MeasureTextCacheItem *hashEntry = Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, elementIndex); + if (hashEntry->id == id) { + hashEntry->generation = context->generation; + return hashEntry; + } + // This element hasn't been seen in a few frames, delete the hash map item + if (context->generation - hashEntry->generation > 2) { + // Add all the measured words that were included in this measurement to the freelist + int32_t nextWordIndex = hashEntry->measuredWordsStartIndex; + while (nextWordIndex != -1) { + Clay__MeasuredWord *measuredWord = Clay__MeasuredWordArray_Get(&context->measuredWords, nextWordIndex); + Clay__int32_tArray_Add(&context->measuredWordsFreeList, nextWordIndex); + nextWordIndex = measuredWord->next; + } + + int32_t nextIndex = hashEntry->nextIndex; + Clay__MeasureTextCacheItemArray_Set(&context->measureTextHashMapInternal, elementIndex, CLAY__INIT(Clay__MeasureTextCacheItem) { .measuredWordsStartIndex = -1 }); + Clay__int32_tArray_Add(&context->measureTextHashMapInternalFreeList, elementIndex); + if (elementIndexPrevious == 0) { + context->measureTextHashMap.internalArray[hashBucket] = nextIndex; + } else { + Clay__MeasureTextCacheItem *previousHashEntry = Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, elementIndexPrevious); + previousHashEntry->nextIndex = nextIndex; + } + elementIndex = nextIndex; + } else { + elementIndexPrevious = elementIndex; + elementIndex = hashEntry->nextIndex; + } + } + + int32_t newItemIndex = 0; + Clay__MeasureTextCacheItem newCacheItem = { .measuredWordsStartIndex = -1, .id = id, .generation = context->generation }; + Clay__MeasureTextCacheItem *measured = NULL; + if (context->measureTextHashMapInternalFreeList.length > 0) { + newItemIndex = Clay__int32_tArray_GetValue(&context->measureTextHashMapInternalFreeList, context->measureTextHashMapInternalFreeList.length - 1); + context->measureTextHashMapInternalFreeList.length--; + Clay__MeasureTextCacheItemArray_Set(&context->measureTextHashMapInternal, newItemIndex, newCacheItem); + measured = Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, newItemIndex); + } else { + if (context->measureTextHashMapInternal.length == context->measureTextHashMapInternal.capacity - 1) { + if (!context->booleanWarnings.maxTextMeasureCacheExceeded) { + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED, + .errorText = CLAY_STRING("Clay ran out of capacity while attempting to measure text elements. Try using Clay_SetMaxElementCount() with a higher value."), + .userData = context->errorHandler.userData }); + context->booleanWarnings.maxTextMeasureCacheExceeded = true; + } + return &Clay__MeasureTextCacheItem_DEFAULT; + } + measured = Clay__MeasureTextCacheItemArray_Add(&context->measureTextHashMapInternal, newCacheItem); + newItemIndex = context->measureTextHashMapInternal.length - 1; + } + + int32_t start = 0; + int32_t end = 0; + float lineWidth = 0; + float measuredWidth = 0; + float measuredHeight = 0; + float spaceWidth = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = 1, .chars = CLAY__SPACECHAR.chars, .baseChars = CLAY__SPACECHAR.chars }, config, context->measureTextUserData).width; + Clay__MeasuredWord tempWord = { .next = -1 }; + Clay__MeasuredWord *previousWord = &tempWord; + while (end < text->length) { + if (context->measuredWords.length == context->measuredWords.capacity - 1) { + if (!context->booleanWarnings.maxTextMeasureCacheExceeded) { + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_TEXT_MEASUREMENT_CAPACITY_EXCEEDED, + .errorText = CLAY_STRING("Clay has run out of space in it's internal text measurement cache. Try using Clay_SetMaxMeasureTextCacheWordCount() (default 16384, with 1 unit storing 1 measured word)."), + .userData = context->errorHandler.userData }); + context->booleanWarnings.maxTextMeasureCacheExceeded = true; + } + return &Clay__MeasureTextCacheItem_DEFAULT; + } + char current = text->chars[end]; + if (current == ' ' || current == '\n') { + int32_t length = end - start; + Clay_Dimensions dimensions = CLAY__DEFAULT_STRUCT; + if (length > 0) { + dimensions = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) {.length = length, .chars = &text->chars[start], .baseChars = text->chars}, config, context->measureTextUserData); + } + measured->minWidth = CLAY__MAX(dimensions.width, measured->minWidth); + measuredHeight = CLAY__MAX(measuredHeight, dimensions.height); + if (current == ' ') { + dimensions.width += spaceWidth; + previousWord = Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = length + 1, .width = dimensions.width, .next = -1 }, previousWord); + lineWidth += dimensions.width; + } + if (current == '\n') { + if (length > 0) { + previousWord = Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = length, .width = dimensions.width, .next = -1 }, previousWord); + } + previousWord = Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = end + 1, .length = 0, .width = 0, .next = -1 }, previousWord); + lineWidth += dimensions.width; + measuredWidth = CLAY__MAX(lineWidth, measuredWidth); + measured->containsNewlines = true; + lineWidth = 0; + } + start = end + 1; + } + end++; + } + if (end - start > 0) { + Clay_Dimensions dimensions = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = end - start, .chars = &text->chars[start], .baseChars = text->chars }, config, context->measureTextUserData); + Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = end - start, .width = dimensions.width, .next = -1 }, previousWord); + lineWidth += dimensions.width; + measuredHeight = CLAY__MAX(measuredHeight, dimensions.height); + measured->minWidth = CLAY__MAX(dimensions.width, measured->minWidth); + } + measuredWidth = CLAY__MAX(lineWidth, measuredWidth) - config->letterSpacing; + + measured->measuredWordsStartIndex = tempWord.next; + measured->unwrappedDimensions.width = measuredWidth; + measured->unwrappedDimensions.height = measuredHeight; + + if (elementIndexPrevious != 0) { + Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, elementIndexPrevious)->nextIndex = newItemIndex; + } else { + context->measureTextHashMap.internalArray[hashBucket] = newItemIndex; + } + return measured; +} + +bool Clay__PointIsInsideRect(Clay_Vector2 point, Clay_BoundingBox rect) { + return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; +} + +Clay_LayoutElementHashMapItem* Clay__AddHashMapItem(Clay_ElementId elementId, Clay_LayoutElement* layoutElement) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->layoutElementsHashMapInternal.length == context->layoutElementsHashMapInternal.capacity - 1) { + if (!context->booleanWarnings.hashMapCapacityExceeded) { + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_HASH_MAP_CAPACITY_EXCEEDED, + .errorText = CLAY_STRING("Clay has run out of space in it's internal element ID hashmap. Try using Clay_SetMaxElementCount() with a higher value."), + .userData = context->errorHandler.userData }); + context->booleanWarnings.hashMapCapacityExceeded = true; + } + return NULL; + } + Clay_LayoutElementHashMapItem item = { .elementId = elementId, .layoutElement = layoutElement, .nextIndex = -1, .generation = context->generation + 1, .appearedThisFrame = true }; + uint32_t hashBucket = elementId.id % context->layoutElementsHashMap.capacity; + int32_t hashItemPrevious = -1; + int32_t hashItemIndex = context->layoutElementsHashMap.internalArray[hashBucket]; + while (hashItemIndex != -1) { // Just replace collision, not a big deal - leave it up to the end user + Clay_LayoutElementHashMapItem *hashItem = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, hashItemIndex); + if (hashItem->elementId.id == elementId.id) { // Collision - resolve based on generation + item.nextIndex = hashItem->nextIndex; + if (hashItem->generation <= context->generation) { // First collision - assume this is the "same" element + hashItem->appearedThisFrame = hashItem->generation < context->generation; + hashItem->elementId = elementId; // Make sure to copy this across. If the stringId reference has changed, we should update the hash item to use the new one. + hashItem->generation = context->generation + 1; + hashItem->layoutElement = layoutElement; + hashItem->debugData.collision = false; + hashItem->onHoverFunction = NULL; + hashItem->hoverFunctionUserData = 0; + } else { // Multiple collisions this frame - two elements have the same ID + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_DUPLICATE_ID, + .errorText = CLAY_STRING("An element with this ID was already previously declared during this layout."), + .userData = context->errorHandler.userData }); + if (context->debugModeEnabled) { + hashItem->debugData.collision = true; + } + } + return hashItem; + } + hashItemPrevious = hashItemIndex; + hashItemIndex = hashItem->nextIndex; + } + + int32_t indexToUse = 0; + if (context->layoutElementsHashMapFreeList.length > 0) { + indexToUse = Clay__int32_tArray_GetValue(&context->layoutElementsHashMapFreeList, context->layoutElementsHashMapFreeList.length - 1); + context->layoutElementsHashMapFreeList.length--; + } else { + indexToUse = context->layoutElementsHashMapInternal.length; + } + Clay_LayoutElementHashMapItem *hashItem = Clay__LayoutElementHashMapItemArray_Set(&context->layoutElementsHashMapInternal, indexToUse, item); + if (hashItemPrevious != -1) { + Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, hashItemPrevious)->nextIndex = (int32_t)indexToUse; + } else { + context->layoutElementsHashMap.internalArray[hashBucket] = (int32_t)indexToUse; + } + return hashItem; +} + +Clay_LayoutElementHashMapItem *Clay__GetHashMapItem(uint32_t id) { + Clay_Context* context = Clay_GetCurrentContext(); + uint32_t hashBucket = id % context->layoutElementsHashMap.capacity; + int32_t elementIndex = context->layoutElementsHashMap.internalArray[hashBucket]; + while (elementIndex != -1) { + Clay_LayoutElementHashMapItem *hashEntry = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, elementIndex); + if (hashEntry->elementId.id == id) { + return hashEntry; + } + elementIndex = hashEntry->nextIndex; + } + return &Clay_LayoutElementHashMapItem_DEFAULT; +} + +void Clay__UpdateAspectRatioBox(Clay_LayoutElement *layoutElement) { + if (layoutElement->config.aspectRatio.aspectRatio != 0) { + if (layoutElement->dimensions.width == 0 && layoutElement->dimensions.height != 0) { + layoutElement->dimensions.width = layoutElement->dimensions.height * layoutElement->config.aspectRatio.aspectRatio; + } else if (layoutElement->dimensions.width != 0 && layoutElement->dimensions.height == 0) { + layoutElement->dimensions.height = layoutElement->dimensions.width * (1 / layoutElement->config.aspectRatio.aspectRatio); + } + } +} + +void Clay__CloseElement(void) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->booleanWarnings.maxElementsExceeded) { + return; + } + Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement(); + Clay_LayoutConfig *layoutConfig = &openLayoutElement->config.layout; + bool elementHasClipHorizontal = openLayoutElement->config.clip.horizontal; + bool elementHasClipVertical = openLayoutElement->config.clip.vertical; + if (elementHasClipHorizontal || elementHasClipVertical || openLayoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) { + context->openClipElementStack.length--; + } + + float leftRightPadding = (float)(layoutConfig->padding.left + layoutConfig->padding.right); + float topBottomPadding = (float)(layoutConfig->padding.top + layoutConfig->padding.bottom); + + // Attach children to the current open element + openLayoutElement->children.elements = &context->layoutElementChildren.internalArray[context->layoutElementChildren.length]; + if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) { + openLayoutElement->dimensions.width = leftRightPadding; + openLayoutElement->minDimensions.width = leftRightPadding; + for (int32_t i = 0; i < openLayoutElement->children.length; i++) { + int32_t childIndex = Clay__int32_tArray_GetValue(&context->layoutElementChildrenBuffer, (int)context->layoutElementChildrenBuffer.length - openLayoutElement->children.length + i); + Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, childIndex); + openLayoutElement->dimensions.width += child->dimensions.width; + openLayoutElement->dimensions.height = CLAY__MAX(openLayoutElement->dimensions.height, child->dimensions.height + topBottomPadding); + // Minimum size of child elements doesn't matter to clip containers as they can shrink and hide their contents + if (!elementHasClipHorizontal) { + openLayoutElement->minDimensions.width += child->minDimensions.width; + } + if (!elementHasClipVertical) { + openLayoutElement->minDimensions.height = CLAY__MAX(openLayoutElement->minDimensions.height, child->minDimensions.height + topBottomPadding); + } + Clay__int32_tArray_Add(&context->layoutElementChildren, childIndex); + } + float childGap = (float)(CLAY__MAX(openLayoutElement->children.length - 1, 0) * layoutConfig->childGap); + openLayoutElement->dimensions.width += childGap; + if (!elementHasClipHorizontal) { + openLayoutElement->minDimensions.width += childGap; + } + } + else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) { + openLayoutElement->dimensions.height = topBottomPadding; + openLayoutElement->minDimensions.height = topBottomPadding; + for (int32_t i = 0; i < openLayoutElement->children.length; i++) { + int32_t childIndex = Clay__int32_tArray_GetValue(&context->layoutElementChildrenBuffer, (int)context->layoutElementChildrenBuffer.length - openLayoutElement->children.length + i); + Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, childIndex); + openLayoutElement->dimensions.height += child->dimensions.height; + openLayoutElement->dimensions.width = CLAY__MAX(openLayoutElement->dimensions.width, child->dimensions.width + leftRightPadding); + // Minimum size of child elements doesn't matter to clip containers as they can shrink and hide their contents + if (!elementHasClipVertical) { + openLayoutElement->minDimensions.height += child->minDimensions.height; + } + if (!elementHasClipHorizontal) { + openLayoutElement->minDimensions.width = CLAY__MAX(openLayoutElement->minDimensions.width, child->minDimensions.width + leftRightPadding); + } + Clay__int32_tArray_Add(&context->layoutElementChildren, childIndex); + } + float childGap = (float)(CLAY__MAX(openLayoutElement->children.length - 1, 0) * layoutConfig->childGap); + openLayoutElement->dimensions.height += childGap; + if (!elementHasClipVertical) { + openLayoutElement->minDimensions.height += childGap; + } + } + + context->layoutElementChildrenBuffer.length -= openLayoutElement->children.length; + + // Clamp element min and max width to the values configured in the layout + if (layoutConfig->sizing.width.type != CLAY__SIZING_TYPE_PERCENT) { + if (layoutConfig->sizing.width.size.minMax.max <= 0) { // Set the max size if the user didn't specify, makes calculations easier + layoutConfig->sizing.width.size.minMax.max = CLAY__MAXFLOAT; + } + openLayoutElement->dimensions.width = CLAY__MIN(CLAY__MAX(openLayoutElement->dimensions.width, layoutConfig->sizing.width.size.minMax.min), layoutConfig->sizing.width.size.minMax.max); + openLayoutElement->minDimensions.width = CLAY__MIN(CLAY__MAX(openLayoutElement->minDimensions.width, layoutConfig->sizing.width.size.minMax.min), layoutConfig->sizing.width.size.minMax.max); + } else { + openLayoutElement->dimensions.width = 0; + } + + // Clamp element min and max height to the values configured in the layout + if (layoutConfig->sizing.height.type != CLAY__SIZING_TYPE_PERCENT) { + if (layoutConfig->sizing.height.size.minMax.max <= 0) { // Set the max size if the user didn't specify, makes calculations easier + layoutConfig->sizing.height.size.minMax.max = CLAY__MAXFLOAT; + } + openLayoutElement->dimensions.height = CLAY__MIN(CLAY__MAX(openLayoutElement->dimensions.height, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max); + openLayoutElement->minDimensions.height = CLAY__MIN(CLAY__MAX(openLayoutElement->minDimensions.height, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max); + } else { + openLayoutElement->dimensions.height = 0; + } + + Clay__UpdateAspectRatioBox(openLayoutElement); + + bool elementIsFloating = openLayoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE; + + // Close the currently open element + int32_t closingElementIndex = Clay__int32_tArray_RemoveSwapback(&context->openLayoutElementStack, (int)context->openLayoutElementStack.length - 1); + + // Get the currently open parent + openLayoutElement = Clay__GetOpenLayoutElement(); + + if (context->openLayoutElementStack.length > 1) { + if(elementIsFloating) { + openLayoutElement->floatingChildrenCount++; + return; + } + openLayoutElement->children.length++; + Clay__int32_tArray_Add(&context->layoutElementChildrenBuffer, closingElementIndex); + } +} + +bool Clay__MemCmp(const char *s1, const char *s2, int32_t length); +#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64)) + bool Clay__MemCmp(const char *s1, const char *s2, int32_t length) { + while (length >= 16) { + __m128i v1 = _mm_loadu_si128((const __m128i *)s1); + __m128i v2 = _mm_loadu_si128((const __m128i *)s2); + + if (_mm_movemask_epi8(_mm_cmpeq_epi8(v1, v2)) != 0xFFFF) { // If any byte differs + return false; + } + + s1 += 16; + s2 += 16; + length -= 16; + } + + // Handle remaining bytes + while (length--) { + if (*s1 != *s2) { + return false; + } + s1++; + s2++; + } + + return true; + } +#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__) + bool Clay__MemCmp(const char *s1, const char *s2, int32_t length) { + while (length >= 16) { + uint8x16_t v1 = vld1q_u8((const uint8_t *)s1); + uint8x16_t v2 = vld1q_u8((const uint8_t *)s2); + + // Compare vectors + if (vminvq_u32(vreinterpretq_u32_u8(vceqq_u8(v1, v2))) != 0xFFFFFFFF) { // If there's a difference + return false; + } + + s1 += 16; + s2 += 16; + length -= 16; + } + + // Handle remaining bytes + while (length--) { + if (*s1 != *s2) { + return false; + } + s1++; + s2++; + } + + return true; + } +#else + bool Clay__MemCmp(const char *s1, const char *s2, int32_t length) { + for (int32_t i = 0; i < length; i++) { + if (s1[i] != s2[i]) { + return false; + } + } + return true; + } +#endif + +void Clay__OpenElement(void) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) { + context->booleanWarnings.maxElementsExceeded = true; + return; + } + Clay_LayoutElement layoutElement = CLAY__DEFAULT_STRUCT; + Clay_LayoutElement* openLayoutElement = Clay_LayoutElementArray_Add(&context->layoutElements, layoutElement); + Clay__int32_tArray_Add(&context->openLayoutElementStack, context->layoutElements.length - 1); + // Generate an ID + Clay_LayoutElement *parentElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2)); + uint32_t offset = parentElement->children.length + parentElement->floatingChildrenCount; + Clay_ElementId elementId = Clay__HashNumber(offset, parentElement->id); + openLayoutElement->id = elementId.id; + Clay__AddHashMapItem(elementId, openLayoutElement); + Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId); + if (context->openClipElementStack.length > 0) { + Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1)); + } else { + Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, 0); + } +} + +void Clay__OpenElementWithId(Clay_ElementId elementId) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) { + context->booleanWarnings.maxElementsExceeded = true; + return; + } + Clay_LayoutElement layoutElement = CLAY__DEFAULT_STRUCT; + layoutElement.id = elementId.id; + Clay_LayoutElement * openLayoutElement = Clay_LayoutElementArray_Add(&context->layoutElements, layoutElement); + Clay__int32_tArray_Add(&context->openLayoutElementStack, context->layoutElements.length - 1); + Clay__AddHashMapItem(elementId, openLayoutElement); + Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId); + if (context->openClipElementStack.length > 0) { + Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1)); + } else { + Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, 0); + } +} + +void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig textConfig) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) { + context->booleanWarnings.maxElementsExceeded = true; + return; + } + Clay_LayoutElement *parentElement = Clay__GetOpenLayoutElement(); + + Clay_LayoutElement layoutElement = { .textConfig = textConfig, .isTextElement = true }; + Clay_LayoutElement *textElement = Clay_LayoutElementArray_Add(&context->layoutElements, layoutElement); + if (context->openClipElementStack.length > 0) { + Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1)); + } else { + Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, 0); + } + + Clay__int32_tArray_Add(&context->layoutElementChildrenBuffer, context->layoutElements.length - 1); + Clay__MeasureTextCacheItem *textMeasured = Clay__MeasureTextCached(&text, &textConfig); + Clay_ElementId elementId = Clay__HashNumber(parentElement->children.length + parentElement->floatingChildrenCount, parentElement->id); + textElement->id = elementId.id; + Clay__AddHashMapItem(elementId, textElement); + Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId); + Clay_Dimensions textDimensions = { .width = textMeasured->unwrappedDimensions.width, .height = textConfig.lineHeight > 0 ? (float)textConfig.lineHeight : textMeasured->unwrappedDimensions.height }; + textElement->dimensions = textDimensions; + textElement->minDimensions = CLAY__INIT(Clay_Dimensions) { .width = textMeasured->minWidth, .height = textDimensions.height }; + textElement->textElementData = CLAY__INIT(Clay__TextElementData) { .text = text, .preferredDimensions = textMeasured->unwrappedDimensions }; + parentElement->children.length++; +} + +void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *declaration) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement(); + openLayoutElement->config = *declaration; + if ((declaration->layout.sizing.width.type == CLAY__SIZING_TYPE_PERCENT && declaration->layout.sizing.width.size.percent > 1) || (declaration->layout.sizing.height.type == CLAY__SIZING_TYPE_PERCENT && declaration->layout.sizing.height.size.percent > 1)) { + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_PERCENTAGE_OVER_1, + .errorText = CLAY_STRING("An element was configured with CLAY_SIZING_PERCENT, but the provided percentage value was over 1.0. Clay expects a value between 0 and 1, i.e. 20% is 0.2."), + .userData = context->errorHandler.userData }); + } + + if (declaration->floating.attachTo != CLAY_ATTACH_TO_NONE) { + Clay_FloatingElementConfig* floatingConfig = &openLayoutElement->config.floating; + // This looks dodgy but because of the auto generated root element the depth of the tree will always be at least 2 here + Clay_LayoutElement *hierarchicalParent = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2)); + if (hierarchicalParent) { + uint32_t clipElementId = 0; + if (declaration->floating.attachTo == CLAY_ATTACH_TO_PARENT) { + // Attach to the element's direct hierarchical parent + floatingConfig->parentId = hierarchicalParent->id; + if (context->openClipElementStack.length > 0) { + clipElementId = Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1); + } + } else if (declaration->floating.attachTo == CLAY_ATTACH_TO_ELEMENT_WITH_ID) { + Clay_LayoutElementHashMapItem *parentItem = Clay__GetHashMapItem(floatingConfig->parentId); + if (parentItem == &Clay_LayoutElementHashMapItem_DEFAULT) { + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_FLOATING_CONTAINER_PARENT_NOT_FOUND, + .errorText = CLAY_STRING("A floating element was declared with a parentId, but no element with that ID was found."), + .userData = context->errorHandler.userData }); + } else { + clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, (int32_t)(parentItem->layoutElement - context->layoutElements.internalArray)); + } + } else if (declaration->floating.attachTo == CLAY_ATTACH_TO_ROOT) { + floatingConfig->parentId = Clay__HashString(CLAY_STRING("Clay__RootContainer"), 0).id; + } + if (declaration->floating.clipTo == CLAY_CLIP_TO_NONE) { + clipElementId = 0; + } + int32_t currentElementIndex = Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1); + Clay__int32_tArray_Set(&context->layoutElementClipElementIds, currentElementIndex, clipElementId); + Clay__int32_tArray_Add(&context->openClipElementStack, clipElementId); + Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) { + .layoutElementIndex = Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1), + .parentId = floatingConfig->parentId, + .clipElementId = clipElementId, + .zIndex = floatingConfig->zIndex, + }); + } + } + + if (declaration->clip.horizontal || declaration->clip.vertical) { + Clay__int32_tArray_Add(&context->openClipElementStack, (int)openLayoutElement->id); + // Retrieve or create cached data to track scroll position across frames + Clay__ScrollContainerDataInternal *scrollOffset = CLAY__NULL; + for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) { + Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i); + if (openLayoutElement->id == mapping->elementId) { + scrollOffset = mapping; + scrollOffset->layoutElement = openLayoutElement; + scrollOffset->openThisFrame = true; + } + } + if (!scrollOffset) { + scrollOffset = Clay__ScrollContainerDataInternalArray_Add(&context->scrollContainerDatas, CLAY__INIT(Clay__ScrollContainerDataInternal){.layoutElement = openLayoutElement, .scrollOrigin = {-1,-1}, .elementId = openLayoutElement->id, .openThisFrame = true}); + } + if (context->externalScrollHandlingEnabled) { + scrollOffset->scrollPosition = Clay__QueryScrollOffset(scrollOffset->elementId, context->queryScrollOffsetUserData); + } + } + // Setup data to track transitions across frames + if (declaration->transition.handler) { + Clay__TransitionDataInternal *transitionData = CLAY__NULL; + Clay_LayoutElement* parentElement = Clay__GetParentElement(); + for (int32_t i = 0; i < context->transitionDatas.length; i++) { + Clay__TransitionDataInternal *existingData = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i); + if (openLayoutElement->id == existingData->elementId) { + if (existingData->state == CLAY_TRANSITION_STATE_EXITING) { + existingData->state = CLAY_TRANSITION_STATE_IDLE; + Clay_LayoutElementHashMapItem* hashMapItem = Clay__GetHashMapItem(openLayoutElement->id); + hashMapItem->appearedThisFrame = false; + } + transitionData = existingData; + transitionData->elementThisFrame = openLayoutElement; + if (transitionData->parentId != parentElement->id) { + transitionData->reparented = true; + } + transitionData->parentId = parentElement->id; + transitionData->siblingIndex = parentElement->children.length; + transitionData->transitionOut = !!declaration->transition.exit.setFinalState; + } + } + if (!transitionData) { + transitionData = Clay__TransitionDataInternalArray_Add(&context->transitionDatas, CLAY__INIT(Clay__TransitionDataInternal){ + .elementThisFrame = openLayoutElement, + .elementId = openLayoutElement->id, + .parentId = parentElement->id, + .siblingIndex = parentElement->children.length, + .transitionOut = !!declaration->transition.exit.setFinalState + }); + } + } +} + +void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) { + Clay__ConfigureOpenElementPtr(&declaration); +} + +void Clay__InitializeEphemeralMemory(Clay_Context* context) { + int32_t maxElementCount = context->maxElementCount; + // Ephemeral Memory - reset every frame + Clay_Arena *arena = &context->internalArena; + arena->nextAllocation = context->arenaResetOffset; + + context->layoutElementChildrenBuffer = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->layoutElements = Clay_LayoutElementArray_Allocate_Arena(maxElementCount, arena); + context->warnings = Clay__WarningArray_Allocate_Arena(100, arena); + + context->layoutElementIdStrings = Clay__StringArray_Allocate_Arena(maxElementCount, arena); + context->wrappedTextLines = Clay__WrappedTextLineArray_Allocate_Arena(maxElementCount, arena); + context->layoutElementTreeNodeArray1 = Clay__LayoutElementTreeNodeArray_Allocate_Arena(maxElementCount, arena); + context->layoutElementTreeRoots = Clay__LayoutElementTreeRootArray_Allocate_Arena(maxElementCount, arena); + context->layoutElementChildren = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->openLayoutElementStack = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->renderCommands = Clay_RenderCommandArray_Allocate_Arena(maxElementCount, arena); + context->treeNodeVisited = Clay__boolArray_Allocate_Arena(maxElementCount, arena); + context->treeNodeVisited.length = context->treeNodeVisited.capacity; // This array is accessed directly rather than behaving as a list + context->openClipElementStack = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->reusableElementIndexBuffer = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->layoutElementClipElementIds = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->dynamicStringData = Clay__charArray_Allocate_Arena(maxElementCount, arena); +} + +void Clay__InitializePersistentMemory(Clay_Context* context) { + // Persistent memory - initialized once and not reset + int32_t maxElementCount = context->maxElementCount; + int32_t maxMeasureTextCacheWordCount = context->maxMeasureTextCacheWordCount; + Clay_Arena *arena = &context->internalArena; + + context->scrollContainerDatas = Clay__ScrollContainerDataInternalArray_Allocate_Arena(100, arena); + context->transitionDatas = Clay__TransitionDataInternalArray_Allocate_Arena(200, arena); + context->layoutElementsHashMapInternal = Clay__LayoutElementHashMapItemArray_Allocate_Arena(maxElementCount, arena); + context->layoutElementsHashMap = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->layoutElementsHashMapFreeList = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->measureTextHashMapInternal = Clay__MeasureTextCacheItemArray_Allocate_Arena(maxElementCount, arena); + context->measureTextHashMapInternalFreeList = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->measuredWordsFreeList = Clay__int32_tArray_Allocate_Arena(maxMeasureTextCacheWordCount, arena); + context->measureTextHashMap = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena); + context->measuredWords = Clay__MeasuredWordArray_Allocate_Arena(maxMeasureTextCacheWordCount, arena); + context->pointerOverIds = Clay_ElementIdArray_Allocate_Arena(maxElementCount, arena); + context->arenaResetOffset = arena->nextAllocation; +} + +const float CLAY__EPSILON = 0.01; + +bool Clay__FloatEqual(float left, float right) { + float subtracted = left - right; + return subtracted < CLAY__EPSILON && subtracted > -CLAY__EPSILON; +} + +Clay_SizingAxis Clay__GetElementSizing(Clay_LayoutElement* element, bool xAxis) { + if (element->isTextElement) { + return CLAY__INIT(Clay_SizingAxis) {}; + } else { + return xAxis ? element->config.layout.sizing.width : element->config.layout.sizing.height; + } +} + +// Writes out the location of text elements to layout elements buffer 1 +void Clay__SizeContainersAlongAxis(bool xAxis, float deltaTime, Clay__int32_tArray* textElementsOut, Clay__int32_tArray* aspectRatioElementsOut) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay__int32_tArray bfsBuffer = context->layoutElementChildrenBuffer; + Clay__int32_tArray resizableContainerBuffer = context->openLayoutElementStack; + for (int32_t rootIndex = 0; rootIndex < context->layoutElementTreeRoots.length; ++rootIndex) { + bfsBuffer.length = 0; + Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex); + Clay_LayoutElement *rootElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)root->layoutElementIndex); + Clay__int32_tArray_Add(&bfsBuffer, (int32_t)root->layoutElementIndex); + + // Size floating containers to their parents + if (rootElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) { + Clay_FloatingElementConfig *floatingElementConfig = &rootElement->config.floating; + Clay_LayoutElementHashMapItem *parentItem = Clay__GetHashMapItem(floatingElementConfig->parentId); + if (parentItem && parentItem != &Clay_LayoutElementHashMapItem_DEFAULT) { + Clay_LayoutElement *parentLayoutElement = parentItem->layoutElement; + switch (rootElement->config.layout.sizing.width.type) { + case CLAY__SIZING_TYPE_GROW: { + rootElement->dimensions.width = parentLayoutElement->dimensions.width; + break; + } + case CLAY__SIZING_TYPE_PERCENT: { + rootElement->dimensions.width = parentLayoutElement->dimensions.width * rootElement->config.layout.sizing.width.size.percent; + break; + } + default: break; + } + switch (rootElement->config.layout.sizing.height.type) { + case CLAY__SIZING_TYPE_GROW: { + rootElement->dimensions.height = parentLayoutElement->dimensions.height; + break; + } + case CLAY__SIZING_TYPE_PERCENT: { + rootElement->dimensions.height = parentLayoutElement->dimensions.height * rootElement->config.layout.sizing.height.size.percent; + break; + } + default: break; + } + } + } + + if (rootElement->config.layout.sizing.width.type != CLAY__SIZING_TYPE_PERCENT) { + rootElement->dimensions.width = CLAY__MIN(CLAY__MAX(rootElement->dimensions.width, rootElement->config.layout.sizing.width.size.minMax.min), rootElement->config.layout.sizing.width.size.minMax.max); + } + if (rootElement->config.layout.sizing.height.type != CLAY__SIZING_TYPE_PERCENT) { + rootElement->dimensions.height = CLAY__MIN(CLAY__MAX(rootElement->dimensions.height, rootElement->config.layout.sizing.height.size.minMax.min), rootElement->config.layout.sizing.height.size.minMax.max); + } + + + for (int32_t i = 0; i < bfsBuffer.length; ++i) { + int32_t parentIndex = Clay__int32_tArray_GetValue(&bfsBuffer, i); + Clay_LayoutElement *parent = Clay_LayoutElementArray_Get(&context->layoutElements, parentIndex); + Clay_LayoutConfig *parentLayoutConfig = &parent->config.layout; + int32_t growContainerCount = 0; + float parentSize = xAxis ? parent->dimensions.width : parent->dimensions.height; + float parentPadding = (float)(xAxis ? (parentLayoutConfig->padding.left + parentLayoutConfig->padding.right) : (parentLayoutConfig->padding.top + parentLayoutConfig->padding.bottom)); + float innerContentSize = 0, totalPaddingAndChildGaps = parentPadding; + bool sizingAlongAxis = (xAxis && parentLayoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) || (!xAxis && parentLayoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM); + resizableContainerBuffer.length = 0; + float parentChildGap = parentLayoutConfig->childGap; + bool isFirstChild = true; + + for (int32_t childOffset = 0; childOffset < parent->children.length; childOffset++) { + int32_t childElementIndex = parent->children.elements[childOffset]; + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, childElementIndex); + Clay_SizingAxis childSizing = Clay__GetElementSizing(childElement, xAxis); + float childSize = xAxis ? childElement->dimensions.width : childElement->dimensions.height; + + if (textElementsOut && childElement->isTextElement) { + Clay__int32_tArray_Add(textElementsOut, childElementIndex); + } else if (childElement->children.length > 0) { + Clay__int32_tArray_Add(&bfsBuffer, childElementIndex); + } + + if (!childElement->isTextElement && aspectRatioElementsOut && childElement->config.aspectRatio.aspectRatio != 0) { + Clay__int32_tArray_Add(aspectRatioElementsOut, childElementIndex); + } + + // Note: setting isFirstChild = false is skipped here + if (childElement->exiting) { + continue; + } + + if (childSizing.type != CLAY__SIZING_TYPE_PERCENT + && childSizing.type != CLAY__SIZING_TYPE_FIXED + && (!childElement->isTextElement || childElement->textConfig.wrapMode == CLAY_TEXT_WRAP_WORDS) +// && (xAxis || !Clay__ElementHasConfig(childElement, CLAY__ELEMENT_CONFIG_TYPE_ASPECT)) + ) { + Clay__int32_tArray_Add(&resizableContainerBuffer, childElementIndex); + } + + if (sizingAlongAxis) { + innerContentSize += (childSizing.type == CLAY__SIZING_TYPE_PERCENT ? 0 : childSize); + if (childSizing.type == CLAY__SIZING_TYPE_GROW) { + growContainerCount++; + } + if (!isFirstChild) { + innerContentSize += parentChildGap; // For children after index 0, the childAxisOffset is the gap from the previous child + totalPaddingAndChildGaps += parentChildGap; + } + } else { + innerContentSize = CLAY__MAX(childSize, innerContentSize); + } + isFirstChild = false; + } + + // Expand percentage containers to size + for (int32_t childOffset = 0; childOffset < parent->children.length; childOffset++) { + int32_t childElementIndex = parent->children.elements[childOffset]; + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, childElementIndex); + Clay_SizingAxis childSizing = Clay__GetElementSizing(childElement, xAxis); + float *childSize = xAxis ? &childElement->dimensions.width : &childElement->dimensions.height; + if (childSizing.type == CLAY__SIZING_TYPE_PERCENT) { + *childSize = (parentSize - totalPaddingAndChildGaps) * childSizing.size.percent; + if (sizingAlongAxis) { + innerContentSize += *childSize; + } + Clay__UpdateAspectRatioBox(childElement); + } + } + + if (sizingAlongAxis) { + float sizeToDistribute = parentSize - parentPadding - innerContentSize; + // The content is too large, compress the children as much as possible + if (sizeToDistribute < 0) { + // If the parent clips content in this axis direction, don't compress children, just leave them alone + if (((xAxis && parent->config.clip.horizontal) || (!xAxis && parent->config.clip.vertical))) { + continue; + } + // Scrolling containers preferentially compress before others + while (sizeToDistribute < -CLAY__EPSILON && resizableContainerBuffer.length > 0) { + float largest = 0; + float secondLargest = 0; + float widthToAdd = sizeToDistribute; + for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) { + Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex)); + float childSize = xAxis ? child->dimensions.width : child->dimensions.height; + if (Clay__FloatEqual(childSize, largest)) { continue; } + if (childSize > largest) { + secondLargest = largest; + largest = childSize; + } + if (childSize < largest) { + secondLargest = CLAY__MAX(secondLargest, childSize); + widthToAdd = secondLargest - largest; + } + } + + widthToAdd = CLAY__MAX(widthToAdd, sizeToDistribute / resizableContainerBuffer.length); + + for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) { + Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex)); + float *childSize = xAxis ? &child->dimensions.width : &child->dimensions.height; + float minSize = xAxis ? child->minDimensions.width : child->minDimensions.height; + float previousWidth = *childSize; + if (Clay__FloatEqual(*childSize, largest)) { + *childSize += widthToAdd; + if (*childSize <= minSize) { + *childSize = minSize; + Clay__int32_tArray_RemoveSwapback(&resizableContainerBuffer, childIndex--); + } + sizeToDistribute -= (*childSize - previousWidth); + } + } + } + // The content is too small, allow SIZING_GROW containers to expand + } else if (sizeToDistribute > 0 && growContainerCount > 0) { + for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) { + Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex)); + Clay__SizingType childSizing = Clay__GetElementSizing(child, xAxis).type; + if (childSizing != CLAY__SIZING_TYPE_GROW) { + Clay__int32_tArray_RemoveSwapback(&resizableContainerBuffer, childIndex--); + } + } + while (sizeToDistribute > CLAY__EPSILON && resizableContainerBuffer.length > 0) { + float smallest = CLAY__MAXFLOAT; + float secondSmallest = CLAY__MAXFLOAT; + float widthToAdd = sizeToDistribute; + for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) { + Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex)); + float childSize = xAxis ? child->dimensions.width : child->dimensions.height; + if (Clay__FloatEqual(childSize, smallest)) { continue; } + if (childSize < smallest) { + secondSmallest = smallest; + smallest = childSize; + } + if (childSize > smallest) { + secondSmallest = CLAY__MIN(secondSmallest, childSize); + widthToAdd = secondSmallest - smallest; + } + } + + widthToAdd = CLAY__MIN(widthToAdd, sizeToDistribute / resizableContainerBuffer.length); + + for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) { + Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex)); + float *childSize = xAxis ? &child->dimensions.width : &child->dimensions.height; + Clay_SizingAxis childSizing = Clay__GetElementSizing(child, xAxis); + float maxSize = childSizing.size.minMax.max; + float previousWidth = *childSize; + if (Clay__FloatEqual(*childSize, smallest)) { + *childSize += widthToAdd; + if (*childSize >= maxSize) { + *childSize = maxSize; + Clay__int32_tArray_RemoveSwapback(&resizableContainerBuffer, childIndex--); + } + sizeToDistribute -= (*childSize - previousWidth); + } + } + } + } + // Sizing along the non layout axis ("off axis") + } else { + for (int32_t childOffset = 0; childOffset < resizableContainerBuffer.length; childOffset++) { + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childOffset)); + Clay_SizingAxis childSizing = Clay__GetElementSizing(childElement, xAxis); + float minSize = xAxis ? childElement->minDimensions.width : childElement->minDimensions.height; + float *childSize = xAxis ? &childElement->dimensions.width : &childElement->dimensions.height; + + float maxSize = parentSize - parentPadding; + // If we're laying out the children of a scroll panel, grow containers expand to the size of the inner content, not the outer container + if (((xAxis && parent->config.clip.horizontal) || (!xAxis && parent->config.clip.vertical))) { + maxSize = CLAY__MAX(maxSize, innerContentSize); + } + if (childSizing.type == CLAY__SIZING_TYPE_GROW) { + *childSize = CLAY__MIN(maxSize, childSizing.size.minMax.max); + } + *childSize = CLAY__MAX(minSize, CLAY__MIN(*childSize, maxSize)); + } + } + } + } +} + +Clay_String Clay__IntToString(int32_t integer) { + if (integer == 0) { + return CLAY__INIT(Clay_String) { .length = 1, .chars = "0" }; + } + Clay_Context* context = Clay_GetCurrentContext(); + char *chars = (char *)(context->dynamicStringData.internalArray + context->dynamicStringData.length); + int32_t length = 0; + int32_t sign = integer; + + if (integer < 0) { + integer = -integer; + } + while (integer > 0) { + chars[length++] = (char)(integer % 10 + '0'); + integer /= 10; + } + + if (sign < 0) { + chars[length++] = '-'; + } + + // Reverse the string to get the correct order + for (int32_t j = 0, k = length - 1; j < k; j++, k--) { + char temp = chars[j]; + chars[j] = chars[k]; + chars[k] = temp; + } + context->dynamicStringData.length += length; + return CLAY__INIT(Clay_String) { .length = length, .chars = chars }; +} + +void Clay__AddRenderCommand(Clay_RenderCommand renderCommand) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->renderCommands.length < context->renderCommands.capacity - 1) { + Clay_RenderCommandArray_Add(&context->renderCommands, renderCommand); + } else { + if (!context->booleanWarnings.maxRenderCommandsExceeded) { + context->booleanWarnings.maxRenderCommandsExceeded = true; + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED, + .errorText = CLAY_STRING("Clay ran out of capacity while attempting to create render commands. This is usually caused by a large amount of wrapping text elements while close to the max element capacity. Try using Clay_SetMaxElementCount() with a higher value."), + .userData = context->errorHandler.userData }); + } + } +} + +bool Clay__ElementIsOffscreen(Clay_BoundingBox *boundingBox) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->disableCulling) { + return false; + } + + return (boundingBox->x > (float)context->layoutDimensions.width) || + (boundingBox->y > (float)context->layoutDimensions.height) || + (boundingBox->x + boundingBox->width < 0) || + (boundingBox->y + boundingBox->height < 0); +} + +void Clay__CalculateFinalLayout(float deltaTime, bool useStoredBoundingBoxes, bool generateRenderCommands) { + Clay_Context* context = Clay_GetCurrentContext(); + + // Calculate sizing along the X axis + Clay__int32_tArray textElements = context->openClipElementStack; + textElements.length = 0; + Clay__int32_tArray aspectRatioElements = context->reusableElementIndexBuffer; + aspectRatioElements.length = 0; + Clay__SizeContainersAlongAxis(true, deltaTime, &textElements, &aspectRatioElements); + + // Wrap text + for (int32_t textElementIndex = 0; textElementIndex < textElements.length; ++textElementIndex) { + Clay_LayoutElement *element = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&textElements, textElementIndex)); + Clay__TextElementData *textElementData = &element->textElementData; + textElementData->wrappedLines = CLAY__INIT(Clay__WrappedTextLineArraySlice) { .length = 0, .internalArray = &context->wrappedTextLines.internalArray[context->wrappedTextLines.length] }; + Clay_LayoutElement *containerElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&textElements, textElementIndex)); + Clay__MeasureTextCacheItem *measureTextCacheItem = Clay__MeasureTextCached(&textElementData->text, &containerElement->textConfig); + float lineWidth = 0; + float lineHeight = containerElement->textConfig.lineHeight > 0 ? (float)containerElement->textConfig.lineHeight : textElementData->preferredDimensions.height; + int32_t lineLengthChars = 0; + int32_t lineStartOffset = 0; + if (!measureTextCacheItem->containsNewlines && textElementData->preferredDimensions.width <= containerElement->dimensions.width) { + Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { containerElement->dimensions, textElementData->text }); + textElementData->wrappedLines.length++; + continue; + } + float spaceWidth = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = 1, .chars = CLAY__SPACECHAR.chars, .baseChars = CLAY__SPACECHAR.chars }, &containerElement->textConfig, context->measureTextUserData).width; + int32_t wordIndex = measureTextCacheItem->measuredWordsStartIndex; + while (wordIndex != -1) { + if (context->wrappedTextLines.length > context->wrappedTextLines.capacity - 1) { + break; + } + Clay__MeasuredWord *measuredWord = Clay__MeasuredWordArray_Get(&context->measuredWords, wordIndex); + // Only word on the line is too large, just render it anyway + if (lineLengthChars == 0 && lineWidth + measuredWord->width > containerElement->dimensions.width) { + Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { { measuredWord->width, lineHeight }, { .length = measuredWord->length, .chars = &textElementData->text.chars[measuredWord->startOffset] } }); + textElementData->wrappedLines.length++; + wordIndex = measuredWord->next; + lineStartOffset = measuredWord->startOffset + measuredWord->length; + } + // measuredWord->length == 0 means a newline character + else if (measuredWord->length == 0 || lineWidth + measuredWord->width > containerElement->dimensions.width) { + // Wrapped text lines list has overflowed, just render out the line + bool finalCharIsSpace = textElementData->text.chars[CLAY__MAX(lineStartOffset + lineLengthChars - 1, 0)] == ' '; + Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { { lineWidth + (finalCharIsSpace ? -spaceWidth : 0), lineHeight }, { .length = lineLengthChars + (finalCharIsSpace ? -1 : 0), .chars = &textElementData->text.chars[lineStartOffset] } }); + textElementData->wrappedLines.length++; + if (lineLengthChars == 0 || measuredWord->length == 0) { + wordIndex = measuredWord->next; + } + lineWidth = 0; + lineLengthChars = 0; + lineStartOffset = measuredWord->startOffset; + } else { + lineWidth += measuredWord->width + containerElement->textConfig.letterSpacing; + lineLengthChars += measuredWord->length; + wordIndex = measuredWord->next; + } + } + if (lineLengthChars > 0) { + Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { { lineWidth - containerElement->textConfig.letterSpacing, lineHeight }, {.length = lineLengthChars, .chars = &textElementData->text.chars[lineStartOffset] } }); + textElementData->wrappedLines.length++; + } + containerElement->dimensions.height = lineHeight * (float)textElementData->wrappedLines.length; + } + + // Scale vertical heights according to aspect ratio + for (int32_t i = 0; i < aspectRatioElements.length; ++i) { + Clay_LayoutElement* aspectElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&aspectRatioElements, i)); + aspectElement->dimensions.height = (1 / aspectElement->config.aspectRatio.aspectRatio) * aspectElement->dimensions.width; + aspectElement->config.layout.sizing.height.size.minMax.max = aspectElement->dimensions.height; + } + + // Propagate effect of text wrapping, aspect scaling etc. on height of parents + Clay__LayoutElementTreeNodeArray dfsBuffer = context->layoutElementTreeNodeArray1; + dfsBuffer.length = 0; + for (int32_t i = 0; i < context->layoutElementTreeRoots.length; ++i) { + Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, i); + context->treeNodeVisited.internalArray[dfsBuffer.length] = false; + Clay__LayoutElementTreeNodeArray_Add(&dfsBuffer, CLAY__INIT(Clay__LayoutElementTreeNode) { .layoutElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)root->layoutElementIndex) }); + } + while (dfsBuffer.length > 0) { + Clay__LayoutElementTreeNode *currentElementTreeNode = Clay__LayoutElementTreeNodeArray_Get(&dfsBuffer, (int)dfsBuffer.length - 1); + Clay_LayoutElement *currentElement = currentElementTreeNode->layoutElement; + if (!context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) { + context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true; + // If the element has no children or is the container for a text element, don't bother inspecting it + if (currentElement->isTextElement || currentElement->children.length == 0) { + dfsBuffer.length--; + continue; + } + // Add the children to the DFS buffer (needs to be pushed in reverse so that stack traversal is in correct layout order) + for (int32_t i = 0; i < currentElement->children.length; i++) { + context->treeNodeVisited.internalArray[dfsBuffer.length] = false; + Clay__LayoutElementTreeNodeArray_Add(&dfsBuffer, CLAY__INIT(Clay__LayoutElementTreeNode) { .layoutElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]) }); + } + continue; + } + dfsBuffer.length--; + + // DFS node has been visited, this is on the way back up to the root + Clay_LayoutConfig *layoutConfig = ¤tElement->config.layout; + if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) { + // Resize any parent containers that have grown in height along their non layout axis + for (int32_t j = 0; j < currentElement->children.length; ++j) { + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[j]); + float childHeightWithPadding = CLAY__MAX(childElement->dimensions.height + layoutConfig->padding.top + layoutConfig->padding.bottom, currentElement->dimensions.height); + currentElement->dimensions.height = CLAY__MIN(CLAY__MAX(childHeightWithPadding, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max); + } + } else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) { + // Resizing along the layout axis + float contentHeight = (float)(layoutConfig->padding.top + layoutConfig->padding.bottom); + for (int32_t j = 0; j < currentElement->children.length; ++j) { + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[j]); + contentHeight += childElement->dimensions.height; + } + contentHeight += (float)(CLAY__MAX(currentElement->children.length - 1, 0) * layoutConfig->childGap); + currentElement->dimensions.height = CLAY__MIN(CLAY__MAX(contentHeight, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max); + } + } + + // Calculate sizing along the Y axis + Clay__SizeContainersAlongAxis(false, deltaTime, NULL, NULL); + + // Scale horizontal widths according to aspect ratio + for (int32_t i = 0; i < aspectRatioElements.length; ++i) { + Clay_LayoutElement* aspectElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&aspectRatioElements, i)); + aspectElement->dimensions.width = aspectElement->config.aspectRatio.aspectRatio * aspectElement->dimensions.height; + } + + // Sort tree roots by z-index + int32_t sortMax = context->layoutElementTreeRoots.length - 1; + while (sortMax > 0) { // todo dumb bubble sort + for (int32_t i = 0; i < sortMax; ++i) { + Clay__LayoutElementTreeRoot current = *Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, i); + Clay__LayoutElementTreeRoot next = *Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, i + 1); + if (next.zIndex < current.zIndex) { + Clay__LayoutElementTreeRootArray_Set(&context->layoutElementTreeRoots, i, next); + Clay__LayoutElementTreeRootArray_Set(&context->layoutElementTreeRoots, i + 1, current); + } + } + sortMax--; + } + + // Calculate final positions and generate render commands + context->renderCommands.length = 0; + dfsBuffer.length = 0; + + for (int32_t rootIndex = 0; rootIndex < context->layoutElementTreeRoots.length; ++rootIndex) { + dfsBuffer.length = 0; + Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex); + Clay_LayoutElement *rootElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)root->layoutElementIndex); + Clay_Vector2 rootPosition = CLAY__DEFAULT_STRUCT; + Clay_LayoutElementHashMapItem *parentHashMapItem = Clay__GetHashMapItem(root->parentId); + // Position root floating containers + if (rootElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE && parentHashMapItem) { + Clay_FloatingElementConfig *config = &rootElement->config.floating; + Clay_Dimensions rootDimensions = rootElement->dimensions; + Clay_BoundingBox parentBoundingBox = parentHashMapItem->boundingBox; + // Set X position + Clay_Vector2 targetAttachPosition = CLAY__DEFAULT_STRUCT; + switch (config->attachPoints.parent) { + case CLAY_ATTACH_POINT_LEFT_TOP: + case CLAY_ATTACH_POINT_LEFT_CENTER: + case CLAY_ATTACH_POINT_LEFT_BOTTOM: targetAttachPosition.x = parentBoundingBox.x; break; + case CLAY_ATTACH_POINT_CENTER_TOP: + case CLAY_ATTACH_POINT_CENTER_CENTER: + case CLAY_ATTACH_POINT_CENTER_BOTTOM: targetAttachPosition.x = parentBoundingBox.x + (parentBoundingBox.width / 2); break; + case CLAY_ATTACH_POINT_RIGHT_TOP: + case CLAY_ATTACH_POINT_RIGHT_CENTER: + case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.x = parentBoundingBox.x + parentBoundingBox.width; break; + } + switch (config->attachPoints.element) { + case CLAY_ATTACH_POINT_LEFT_TOP: + case CLAY_ATTACH_POINT_LEFT_CENTER: + case CLAY_ATTACH_POINT_LEFT_BOTTOM: break; + case CLAY_ATTACH_POINT_CENTER_TOP: + case CLAY_ATTACH_POINT_CENTER_CENTER: + case CLAY_ATTACH_POINT_CENTER_BOTTOM: targetAttachPosition.x -= (rootDimensions.width / 2); break; + case CLAY_ATTACH_POINT_RIGHT_TOP: + case CLAY_ATTACH_POINT_RIGHT_CENTER: + case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.x -= rootDimensions.width; break; + } + switch (config->attachPoints.parent) { // I know I could merge the x and y switch statements, but this is easier to read + case CLAY_ATTACH_POINT_LEFT_TOP: + case CLAY_ATTACH_POINT_RIGHT_TOP: + case CLAY_ATTACH_POINT_CENTER_TOP: targetAttachPosition.y = parentBoundingBox.y; break; + case CLAY_ATTACH_POINT_LEFT_CENTER: + case CLAY_ATTACH_POINT_CENTER_CENTER: + case CLAY_ATTACH_POINT_RIGHT_CENTER: targetAttachPosition.y = parentBoundingBox.y + (parentBoundingBox.height / 2); break; + case CLAY_ATTACH_POINT_LEFT_BOTTOM: + case CLAY_ATTACH_POINT_CENTER_BOTTOM: + case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.y = parentBoundingBox.y + parentBoundingBox.height; break; + } + switch (config->attachPoints.element) { + case CLAY_ATTACH_POINT_LEFT_TOP: + case CLAY_ATTACH_POINT_RIGHT_TOP: + case CLAY_ATTACH_POINT_CENTER_TOP: break; + case CLAY_ATTACH_POINT_LEFT_CENTER: + case CLAY_ATTACH_POINT_CENTER_CENTER: + case CLAY_ATTACH_POINT_RIGHT_CENTER: targetAttachPosition.y -= (rootDimensions.height / 2); break; + case CLAY_ATTACH_POINT_LEFT_BOTTOM: + case CLAY_ATTACH_POINT_CENTER_BOTTOM: + case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.y -= rootDimensions.height; break; + } + targetAttachPosition.x += config->offset.x; + targetAttachPosition.y += config->offset.y; + rootPosition = targetAttachPosition; + } + if (root->clipElementId) { + Clay_LayoutElementHashMapItem *clipHashMapItem = Clay__GetHashMapItem(root->clipElementId); + if (clipHashMapItem && !Clay__ElementIsOffscreen(&clipHashMapItem->boundingBox)) { + // Floating elements that are attached to scrolling contents won't be correctly positioned if external scroll handling is enabled, fix here + if (context->externalScrollHandlingEnabled) { + if (clipHashMapItem->layoutElement->config.clip.horizontal) { + rootPosition.x += clipHashMapItem->layoutElement->config.clip.childOffset.x; + } + if (clipHashMapItem->layoutElement->config.clip.vertical) { + rootPosition.y += clipHashMapItem->layoutElement->config.clip.childOffset.y; + } + } + if (generateRenderCommands) { + Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) { + .boundingBox = clipHashMapItem->boundingBox, + .userData = 0, + .id = Clay__HashNumber(rootElement->id, rootElement->children.length + 10).id, // TODO need a better strategy for managing derived ids + .zIndex = root->zIndex, + .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_START, + }); + } + } + } + Clay__LayoutElementTreeNodeArray_Add(&dfsBuffer, CLAY__INIT(Clay__LayoutElementTreeNode) { .layoutElement = rootElement, .position = rootPosition, .nextChildOffset = { .x = (float)rootElement->config.layout.padding.left, .y = (float)rootElement->config.layout.padding.top } }); + + context->treeNodeVisited.internalArray[0] = false; + while (dfsBuffer.length > 0) { + Clay__LayoutElementTreeNode *currentElementTreeNode = Clay__LayoutElementTreeNodeArray_Get(&dfsBuffer, (int)dfsBuffer.length - 1); + Clay_LayoutElement *currentElement = currentElementTreeNode->layoutElement; + Clay_LayoutConfig *layoutConfig = currentElement->isTextElement ? &CLAY_LAYOUT_DEFAULT : ¤tElement->config.layout; + Clay_Vector2 scrollOffset = CLAY__DEFAULT_STRUCT; + + // DFS is returning back upwards + if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) { + if (currentElement->isTextElement) { + dfsBuffer.length--; + continue; + } + Clay_LayoutElementHashMapItem *currentElementData = Clay__GetHashMapItem(currentElement->id); + if (generateRenderCommands && !Clay__ElementIsOffscreen(¤tElementData->boundingBox)) { + // DFS is returning upwards backwards + bool closeClipElement = false; + if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) { + closeClipElement = true; + for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) { + Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i); + if (mapping->layoutElement == currentElement) { + scrollOffset = currentElement->config.clip.childOffset; + if (context->externalScrollHandlingEnabled) { + scrollOffset = CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT; + } + break; + } + } + } + + if (Clay__BorderHasAnyWidth(¤tElement->config.border)) { + Clay_BoundingBox currentElementBoundingBox = currentElementData->boundingBox; + Clay_BorderElementConfig *borderConfig = ¤tElement->config.border; + Clay_RenderCommand renderCommand = { + .boundingBox = currentElementBoundingBox, + .renderData = { .border = { + .color = borderConfig->color, + .cornerRadius = currentElement->config.cornerRadius, + .width = borderConfig->width + }}, + .userData = currentElement->config.userData, + .id = Clay__HashNumber(currentElement->id, currentElement->children.length).id, + .commandType = CLAY_RENDER_COMMAND_TYPE_BORDER, + }; + Clay__AddRenderCommand(renderCommand); + if (borderConfig->width.betweenChildren > 0 && borderConfig->color.a > 0) { + float halfGap = layoutConfig->childGap / 2; + float halfWidth = borderConfig->width.betweenChildren / 2; + Clay_Vector2 borderOffset = { (float)layoutConfig->padding.left - halfGap, (float)layoutConfig->padding.top - halfGap }; + if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) { + for (int32_t i = 0; i < currentElement->children.length; ++i) { + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]); + if (i > 0) { + Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) { + .boundingBox = { currentElementBoundingBox.x + borderOffset.x + scrollOffset.x - halfWidth, currentElementBoundingBox.y + scrollOffset.y, (float)borderConfig->width.betweenChildren, currentElement->dimensions.height }, + .renderData = { .rectangle = { + .backgroundColor = borderConfig->color, + } }, + .userData = currentElement->config.userData, + .id = Clay__HashNumber(currentElement->id, currentElement->children.length + 1 + i).id, + .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE, + }); + } + borderOffset.x += (childElement->dimensions.width + (float)layoutConfig->childGap); + } + } else { + for (int32_t i = 0; i < currentElement->children.length; ++i) { + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]); + if (i > 0) { + Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) { + .boundingBox = { currentElementBoundingBox.x + scrollOffset.x, currentElementBoundingBox.y + borderOffset.y + scrollOffset.y - halfWidth, currentElement->dimensions.width, (float)borderConfig->width.betweenChildren }, + .renderData = { .rectangle = { + .backgroundColor = borderConfig->color, + } }, + .userData = currentElement->config.userData, + .id = Clay__HashNumber(currentElement->id, currentElement->children.length + 1 + i).id, + .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE, + }); + } + borderOffset.y += (childElement->dimensions.height + (float)layoutConfig->childGap); + } + } + } + } + if (currentElement->config.overlayColor.a > 0) { + Clay_RenderCommand renderCommand = { + .userData = currentElement->config.userData, + .id = currentElement->id, + .zIndex = root->zIndex, + .commandType = CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END, + }; + Clay__AddRenderCommand(renderCommand); + } + // This exists because the scissor needs to end _after_ borders between elements + if (closeClipElement) { + Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) { + .id = Clay__HashNumber(currentElement->id, rootElement->children.length + 11).id, + .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_END, + }); + } + } + + dfsBuffer.length--; + continue; + } + + // This will only be run a single time for each element in downwards DFS order + context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true; + Clay_BoundingBox currentElementBoundingBox = { currentElementTreeNode->position.x, currentElementTreeNode->position.y, currentElement->dimensions.width, currentElement->dimensions.height }; + Clay__ScrollContainerDataInternal *scrollContainerData = CLAY__NULL; + if (!currentElement->isTextElement) { + if (useStoredBoundingBoxes && currentElement->config.transition.handler) { + bool found = false; + for (int j = 0; j < context->transitionDatas.length; ++j) { + Clay__TransitionDataInternal* transitionData = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, j); + if (transitionData->elementId == currentElement->id) { + found = true; + if (transitionData->state != CLAY_TRANSITION_STATE_IDLE) { + if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_X) != 0) currentElementBoundingBox.x = transitionData->currentState.boundingBox.x; + if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_Y) != 0) currentElementBoundingBox.y = transitionData->currentState.boundingBox.y; + if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_WIDTH) != 0) currentElementBoundingBox.width = transitionData->currentState.boundingBox.width; + if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_HEIGHT) != 0) currentElementBoundingBox.height = transitionData->currentState.boundingBox.height; + } + break; + } + } + // An exiting element that completed its transition this frame - skip tree + if (!found && currentElement->config.transition.exit.setFinalState) { + dfsBuffer.length--; + continue; + } + } + if (currentElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) { + Clay_FloatingElementConfig *floatingElementConfig = ¤tElement->config.floating; + Clay_Dimensions expand = floatingElementConfig->expand; + currentElementBoundingBox.x -= expand.width; + currentElementBoundingBox.width += expand.width * 2; + currentElementBoundingBox.y -= expand.height; + currentElementBoundingBox.height += expand.height * 2; + } + + // Apply scroll offsets to container + if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) { + // This linear scan could theoretically be slow under very strange conditions, but I can't imagine a real UI with more than a few 10's of scroll containers + for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) { + Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i); + if (mapping->layoutElement == currentElement) { + scrollContainerData = mapping; + mapping->boundingBox = currentElementBoundingBox; + scrollOffset = currentElement->config.clip.childOffset; + if (context->externalScrollHandlingEnabled) { + scrollOffset = CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT; + } + break; + } + } + } + } + + bool offscreen = Clay__ElementIsOffscreen(¤tElementBoundingBox); + + // Generate render commands for current element + if (generateRenderCommands && !offscreen) { + if (currentElement->isTextElement) { + Clay_TextElementConfig *textElementConfig = ¤tElement->textConfig; + float naturalLineHeight = currentElement->textElementData.preferredDimensions.height; + float finalLineHeight = textElementConfig->lineHeight > 0 ? (float)textElementConfig->lineHeight : naturalLineHeight; + float lineHeightOffset = (finalLineHeight - naturalLineHeight) / 2; + float yPosition = lineHeightOffset; + for (int32_t lineIndex = 0; lineIndex < currentElement->textElementData.wrappedLines.length; ++lineIndex) { + Clay__WrappedTextLine *wrappedLine = Clay__WrappedTextLineArraySlice_Get(¤tElement->textElementData.wrappedLines, lineIndex); + if (wrappedLine->line.length == 0) { + yPosition += finalLineHeight; + continue; + } + float offset = (currentElementBoundingBox.width - wrappedLine->dimensions.width); + if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_LEFT) { + offset = 0; + } + if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_CENTER) { + offset /= 2; + } + Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) { + .boundingBox = { currentElementBoundingBox.x + offset, currentElementBoundingBox.y + yPosition, wrappedLine->dimensions.width, wrappedLine->dimensions.height }, + .renderData = { .text = { + .stringContents = CLAY__INIT(Clay_StringSlice) { .length = wrappedLine->line.length, .chars = wrappedLine->line.chars, .baseChars = currentElement->textElementData.text.chars }, + .textColor = textElementConfig->textColor, + .fontId = textElementConfig->fontId, + .fontSize = textElementConfig->fontSize, + .letterSpacing = textElementConfig->letterSpacing, + .lineHeight = textElementConfig->lineHeight, + }}, + .userData = textElementConfig->userData, + .id = Clay__HashNumber(lineIndex, currentElement->id).id, + .zIndex = root->zIndex, + .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT, + }); + yPosition += finalLineHeight; + + if (!context->disableCulling && (currentElementBoundingBox.y + yPosition > context->layoutDimensions.height)) { + break; + } + } + } else { + if (currentElement->config.overlayColor.a > 0) { + Clay_RenderCommand renderCommand = { + .renderData = { + .overlayColor = { .color = currentElement->config.overlayColor } + }, + .userData = currentElement->config.userData, + .id = currentElement->id, + .zIndex = root->zIndex, + .commandType = CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START, + }; + Clay__AddRenderCommand(renderCommand); + } + if (currentElement->config.image.imageData) { + Clay_RenderCommand renderCommand = { + .boundingBox = currentElementBoundingBox, + .renderData = { + .image = { + .backgroundColor = currentElement->config.backgroundColor, + .cornerRadius = currentElement->config.cornerRadius, + .imageData = currentElement->config.image.imageData, + } + }, + .userData = currentElement->config.userData, + .id = currentElement->id, + .zIndex = root->zIndex, + .commandType = CLAY_RENDER_COMMAND_TYPE_IMAGE, + }; + Clay__AddRenderCommand(renderCommand); + } + if (currentElement->config.custom.customData) { + Clay_RenderCommand renderCommand = { + .boundingBox = currentElementBoundingBox, + .renderData = { + .custom = { + .backgroundColor = currentElement->config.backgroundColor, + .cornerRadius = currentElement->config.cornerRadius, + .customData = currentElement->config.custom.customData, + } + }, + .userData = currentElement->config.userData, + .id = currentElement->id, + .zIndex = root->zIndex, + .commandType = CLAY_RENDER_COMMAND_TYPE_CUSTOM, + }; + Clay__AddRenderCommand(renderCommand); + } + if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) { + Clay_RenderCommand renderCommand = { + .boundingBox = currentElementBoundingBox, + .renderData = { + .clip = { + .horizontal = currentElement->config.clip.horizontal, + .vertical = currentElement->config.clip.vertical, + } + }, + .userData = currentElement->config.userData, + .id = currentElement->id, + .zIndex = root->zIndex, + .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_START, + }; + Clay__AddRenderCommand(renderCommand); + } + if (currentElement->config.backgroundColor.a > 0) { + Clay_RenderCommand renderCommand = { + .boundingBox = currentElementBoundingBox, + .renderData = { .rectangle = { + .backgroundColor = currentElement->config.backgroundColor, + .cornerRadius = currentElement->config.cornerRadius, + } }, + .userData = currentElement->config.userData, + .id = currentElement->id, + .zIndex = root->zIndex, + .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE, + }; + Clay__AddRenderCommand(renderCommand); + } + } + } + + Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(currentElement->id); + hashMapItem->boundingBox = currentElementBoundingBox; + + if (currentElement->isTextElement) continue; + + // Setup positions for child elements and add to DFS buffer ---------- + + // On-axis alignment + Clay_Dimensions contentSizeCurrent = {}; + if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) { + for (int32_t i = 0; i < currentElement->children.length; ++i) { + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]); + if (childElement->exiting) continue; + contentSizeCurrent.width += childElement->dimensions.width; + contentSizeCurrent.height = CLAY__MAX(contentSizeCurrent.height, childElement->dimensions.height); + } + contentSizeCurrent.width += (float)(CLAY__MAX(currentElement->children.length - 1, 0) * layoutConfig->childGap); + float extraSpace = currentElement->dimensions.width - (float)(layoutConfig->padding.left + layoutConfig->padding.right) - contentSizeCurrent.width; + switch (layoutConfig->childAlignment.x) { + case CLAY_ALIGN_X_LEFT: extraSpace = 0; break; + case CLAY_ALIGN_X_CENTER: extraSpace /= 2; break; + default: break; + } + extraSpace = CLAY__MAX(0, extraSpace); + currentElementTreeNode->nextChildOffset.x += extraSpace; + } else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) { + for (int32_t i = 0; i < currentElement->children.length; ++i) { + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]); + if (childElement->exiting) continue; + contentSizeCurrent.width = CLAY__MAX(contentSizeCurrent.width, childElement->dimensions.width); + contentSizeCurrent.height += childElement->dimensions.height; + } + contentSizeCurrent.height += (float)(CLAY__MAX(currentElement->children.length - 1, 0) * layoutConfig->childGap); + float extraSpace = currentElement->dimensions.height - (float)(layoutConfig->padding.top + layoutConfig->padding.bottom) - contentSizeCurrent.height; + switch (layoutConfig->childAlignment.y) { + case CLAY_ALIGN_Y_TOP: extraSpace = 0; break; + case CLAY_ALIGN_Y_CENTER: extraSpace /= 2; break; + default: break; + } + extraSpace = CLAY__MAX(0, extraSpace); + currentElementTreeNode->nextChildOffset.y += extraSpace; + } + + if (scrollContainerData) { + scrollContainerData->contentSize = CLAY__INIT(Clay_Dimensions) {contentSizeCurrent.width + (float)(layoutConfig->padding.left + layoutConfig->padding.right), contentSizeCurrent.height + (float)(layoutConfig->padding.top + layoutConfig->padding.bottom) }; + } + + // Add children to the DFS buffer + dfsBuffer.length += currentElement->children.length; + for (int32_t i = 0; i < currentElement->children.length; ++i) { + Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]); + Clay_LayoutElementHashMapItem* childMapItem = Clay__GetHashMapItem(childElement->id); + // Alignment along non layout axis + if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) { + currentElementTreeNode->nextChildOffset.y = currentElement->config.layout.padding.top; + float whiteSpaceAroundChild = currentElement->dimensions.height - (float)(layoutConfig->padding.top + layoutConfig->padding.bottom) - childElement->dimensions.height; + switch (layoutConfig->childAlignment.y) { + case CLAY_ALIGN_Y_TOP: break; + case CLAY_ALIGN_Y_CENTER: currentElementTreeNode->nextChildOffset.y += whiteSpaceAroundChild / 2; break; + case CLAY_ALIGN_Y_BOTTOM: currentElementTreeNode->nextChildOffset.y += whiteSpaceAroundChild; break; + } + } else { + currentElementTreeNode->nextChildOffset.x = currentElement->config.layout.padding.left; + float whiteSpaceAroundChild = currentElement->dimensions.width - (float)(layoutConfig->padding.left + layoutConfig->padding.right) - childElement->dimensions.width; + switch (layoutConfig->childAlignment.x) { + case CLAY_ALIGN_X_LEFT: break; + case CLAY_ALIGN_X_CENTER: currentElementTreeNode->nextChildOffset.x += whiteSpaceAroundChild / 2; break; + case CLAY_ALIGN_X_RIGHT: currentElementTreeNode->nextChildOffset.x += whiteSpaceAroundChild; break; + } + } + + Clay_Vector2 childPosition = { + currentElementBoundingBox.x + currentElementTreeNode->nextChildOffset.x + scrollOffset.x, + currentElementBoundingBox.y + currentElementTreeNode->nextChildOffset.y + scrollOffset.y, + }; + + // DFS buffer elements need to be added in reverse because stack traversal happens backwards + uint32_t newNodeIndex = dfsBuffer.length - 1 - i; + dfsBuffer.internalArray[newNodeIndex] = CLAY__INIT(Clay__LayoutElementTreeNode) { + .layoutElement = childElement, + .position = CLAY__INIT(Clay_Vector2) { childPosition.x, childPosition.y }, + .nextChildOffset = { .x = (float)childElement->config.layout.padding.left, .y = (float)childElement->config.layout.padding.top }, + }; + context->treeNodeVisited.internalArray[newNodeIndex] = false; + + // Update parent offsets + if (!childElement->exiting) { + if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) { + currentElementTreeNode->nextChildOffset.x += childElement->dimensions.width + (float)layoutConfig->childGap; + } else { + currentElementTreeNode->nextChildOffset.y += childElement->dimensions.height + (float)layoutConfig->childGap; + } + } + } + } + + if (root->clipElementId) { + Clay_LayoutElementHashMapItem *clipHashMapItem = Clay__GetHashMapItem(root->clipElementId); + if (clipHashMapItem && !Clay__ElementIsOffscreen(&clipHashMapItem->boundingBox)) { + Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) { .id = Clay__HashNumber(rootElement->id, rootElement->children.length + 11).id, .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_END }); + } + } + } +} + +CLAY_WASM_EXPORT("Clay_GetPointerOverIds") +CLAY_DLL_EXPORT Clay_ElementIdArray Clay_GetPointerOverIds(void) { + return Clay_GetCurrentContext()->pointerOverIds; +} + +#pragma region DebugTools +Clay_Color CLAY__DEBUGVIEW_COLOR_1 = {58, 56, 52, 255}; +Clay_Color CLAY__DEBUGVIEW_COLOR_2 = {62, 60, 58, 255}; +Clay_Color CLAY__DEBUGVIEW_COLOR_3 = {141, 133, 135, 255}; +Clay_Color CLAY__DEBUGVIEW_COLOR_4 = {238, 226, 231, 255}; +Clay_Color CLAY__DEBUGVIEW_COLOR_SELECTED_ROW = {102, 80, 78, 255}; +const int32_t CLAY__DEBUGVIEW_ROW_HEIGHT = 30; +const int32_t CLAY__DEBUGVIEW_OUTER_PADDING = 10; +const int32_t CLAY__DEBUGVIEW_INDENT_WIDTH = 16; +Clay_TextElementConfig Clay__DebugView_TextNameConfig = {.textColor = {238, 226, 231, 255}, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE }; +Clay_LayoutConfig Clay__DebugView_ScrollViewItemLayoutConfig = CLAY__DEFAULT_STRUCT; + +typedef struct { + Clay_String label; + Clay_Color color; +} Clay__DebugElementConfigTypeLabelConfig; + +typedef enum { + CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR, + CLAY__ELEMENT_CONFIG_TYPE_OVERLAY_COLOR, + CLAY__ELEMENT_CONFIG_TYPE_CORNER_RADIUS, + CLAY__ELEMENT_CONFIG_TYPE_TEXT, + CLAY__ELEMENT_CONFIG_TYPE_ASPECT, + CLAY__ELEMENT_CONFIG_TYPE_IMAGE, + CLAY__ELEMENT_CONFIG_TYPE_FLOATING, + CLAY__ELEMENT_CONFIG_TYPE_CLIP, + CLAY__ELEMENT_CONFIG_TYPE_BORDER, + CLAY__ELEMENT_CONFIG_TYPE_CUSTOM, +} Clay__DebugElementConfigType; + +Clay__DebugElementConfigTypeLabelConfig Clay__DebugGetElementConfigTypeLabel(Clay__DebugElementConfigType type) { + switch (type) { + case CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Background"), {243,134,48,255} }; + case CLAY__ELEMENT_CONFIG_TYPE_OVERLAY_COLOR: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Overlay"), { 142,129,206, 255} }; + case CLAY__ELEMENT_CONFIG_TYPE_CORNER_RADIUS: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) {CLAY_STRING("Radius"), {239,148,157, 255 } }; + case CLAY__ELEMENT_CONFIG_TYPE_TEXT: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Text"), {105,210,231,255} }; + case CLAY__ELEMENT_CONFIG_TYPE_ASPECT: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Aspect"), {101,149,194,255} }; + case CLAY__ELEMENT_CONFIG_TYPE_IMAGE: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Image"), {121,189,154,255} }; + case CLAY__ELEMENT_CONFIG_TYPE_FLOATING: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Floating"), {250,105,0,255} }; + case CLAY__ELEMENT_CONFIG_TYPE_CLIP: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) {CLAY_STRING("Scroll"), {242, 196, 90, 255} }; + case CLAY__ELEMENT_CONFIG_TYPE_BORDER: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) {CLAY_STRING("Border"), {108, 91, 123, 255} }; + case CLAY__ELEMENT_CONFIG_TYPE_CUSTOM: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Custom"), {11,72,107,255} }; + default: break; + } + return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Error"), {0,0,0,255} }; +} + +void Clay__RenderElementConfigTypeLabel(Clay_String label, Clay_Color color, bool offscreen) { + Clay_Color backgroundColor = color; + backgroundColor.a = 90; + CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = backgroundColor, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = color, .width = { 1, 1, 1, 1, 0 } } }) { + CLAY_TEXT(label, CLAY_TEXT_CONFIG({ .textColor = offscreen ? CLAY__DEBUGVIEW_COLOR_3 : CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 })); + } +} + +typedef struct { + int32_t rowCount; + int32_t selectedElementRowIndex; +} Clay__RenderDebugLayoutData; + +// Returns row count +Clay__RenderDebugLayoutData Clay__RenderDebugLayoutElementsList(int32_t initialRootsLength, int32_t highlightedRowIndex) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay__int32_tArray dfsBuffer = context->reusableElementIndexBuffer; + Clay__DebugView_ScrollViewItemLayoutConfig = CLAY__INIT(Clay_LayoutConfig) { .sizing = { .height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT) }, .childGap = 6, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }}; + Clay__RenderDebugLayoutData layoutData = CLAY__DEFAULT_STRUCT; + + uint32_t highlightedElementId = 0; + + for (int32_t rootIndex = 0; rootIndex < initialRootsLength; ++rootIndex) { + dfsBuffer.length = 0; + Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex); + Clay__int32_tArray_Add(&dfsBuffer, (int32_t)root->layoutElementIndex); + context->treeNodeVisited.internalArray[0] = false; + if (rootIndex > 0) { + CLAY(CLAY_IDI("Clay__DebugView_EmptyRowOuter", rootIndex), { .layout = { .sizing = {.width = CLAY_SIZING_GROW(0)}, .padding = {CLAY__DEBUGVIEW_INDENT_WIDTH / 2, 0, 0, 0} } }) { + CLAY(CLAY_IDI("Clay__DebugView_EmptyRow", rootIndex), { .layout = { .sizing = { .width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED((float)CLAY__DEBUGVIEW_ROW_HEIGHT) }}, .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .top = 1 } } }) {} + } + layoutData.rowCount++; + } + while (dfsBuffer.length > 0) { + int32_t currentElementIndex = Clay__int32_tArray_GetValue(&dfsBuffer, (int)dfsBuffer.length - 1); + Clay_LayoutElement *currentElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)currentElementIndex); + if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) { + if (!currentElement->isTextElement && currentElement->children.length > 0) { + Clay__CloseElement(); + Clay__CloseElement(); + Clay__CloseElement(); + } + dfsBuffer.length--; + continue; + } + + if (currentElement->exiting) { // TODO there is a duplicate ID problem with exiting elements + dfsBuffer.length--; + continue; + } + + if (highlightedRowIndex == layoutData.rowCount) { + if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) { + context->debugSelectedElementId = currentElement->id; + } + highlightedElementId = currentElement->id; + } + + context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true; + Clay_LayoutElementHashMapItem *currentElementData = Clay__GetHashMapItem(currentElement->id); + bool offscreen = Clay__ElementIsOffscreen(¤tElementData->boundingBox); + if (context->debugSelectedElementId == currentElement->id) { + layoutData.selectedElementRowIndex = layoutData.rowCount; + } + CLAY(CLAY_IDI("Clay__DebugView_ElementOuter", currentElement->id), { .layout = Clay__DebugView_ScrollViewItemLayoutConfig }) { + // Collapse icon / button + if (!(currentElement->isTextElement || currentElement->children.length == 0)) { + CLAY(CLAY_IDI("Clay__DebugView_CollapseElement", currentElement->id), { + .layout = { .sizing = {CLAY_SIZING_FIXED(16), CLAY_SIZING_FIXED(16)}, .childAlignment = { CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER} }, + .cornerRadius = CLAY_CORNER_RADIUS(4), + .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = {1, 1, 1, 1, 0} }, + }) { + CLAY_TEXT((currentElementData && currentElementData->debugData.collapsed) ? CLAY_STRING("+") : CLAY_STRING("-"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 })); + } + } else { // Square dot for empty containers + CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_FIXED(16), CLAY_SIZING_FIXED(16)}, .childAlignment = { CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER } } }) { + CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_FIXED(8), CLAY_SIZING_FIXED(8)} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_3, .cornerRadius = CLAY_CORNER_RADIUS(2) }) {} + } + } + // Collisions and offscreen info + if (currentElementData) { + if (currentElementData->debugData.collision) { + CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 }}, .border = { .color = {177, 147, 8, 255}, .width = {1, 1, 1, 1, 0} } }) { + CLAY_TEXT(CLAY_STRING("Duplicate ID"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 })); + } + } + if (offscreen) { + CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { 1, 1, 1, 1, 0} } }) { + CLAY_TEXT(CLAY_STRING("Offscreen"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 })); + } + } + } + if (currentElementData->elementId.stringId.length > 0) { + CLAY_AUTO_ID() { + Clay_TextElementConfig textConfig = offscreen ? CLAY__INIT(Clay_TextElementConfig) { .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 } : Clay__DebugView_TextNameConfig; + CLAY_TEXT(currentElementData->elementId.stringId, textConfig); + if (currentElementData->elementId.offset != 0) { + CLAY_TEXT(CLAY_STRING(" ("), textConfig); + CLAY_TEXT(Clay__IntToString(currentElementData->elementId.offset), textConfig); + CLAY_TEXT(CLAY_STRING(")"), textConfig); + } + } + } + if (currentElement->isTextElement) { + Clay__RenderElementConfigTypeLabel(CLAY_STRING("Text"), CLAY__INIT(Clay_Color) { 105,210,231,255 }, offscreen); + } else { + if (currentElement->config.backgroundColor.a > 0) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR); + Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen); + } + if (currentElement->config.overlayColor.a > 0) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_OVERLAY_COLOR); + Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen); + } + if (!Clay__MemCmp((const char*)¤tElement->config.cornerRadius, (const char*)&Clay__CornerRadius_DEFAULT, sizeof(Clay_CornerRadius))) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_CORNER_RADIUS); + Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen); + } + if (currentElement->config.aspectRatio.aspectRatio != 0) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_ASPECT); + Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen); + } + if (currentElement->config.image.imageData) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_IMAGE); + Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen); + } + if (currentElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_FLOATING); + Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen); + } + if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_CLIP); + Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen); + } + if (Clay__BorderHasAnyWidth(¤tElement->config.border)) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_BORDER); + Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen); + } + if (currentElement->config.custom.customData) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_CUSTOM); + Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen); + } + } + } + + // Render the text contents below the element as a non-interactive row + if (currentElement->isTextElement) { + layoutData.rowCount++; + Clay__TextElementData *textElementData = ¤tElement->textElementData; + Clay_TextElementConfig rawTextConfig = offscreen ? CLAY__INIT(Clay_TextElementConfig) { .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 } : Clay__DebugView_TextNameConfig; + CLAY_AUTO_ID({ .layout = { .sizing = { .height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } } }) { + CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_INDENT_WIDTH + 16) } } }) {} + CLAY_TEXT(CLAY_STRING("\""), rawTextConfig); + CLAY_TEXT(textElementData->text.length > 40 ? (CLAY__INIT(Clay_String) { .length = 40, .chars = textElementData->text.chars }) : textElementData->text, rawTextConfig); + if (textElementData->text.length > 40) { + CLAY_TEXT(CLAY_STRING("..."), rawTextConfig); + } + CLAY_TEXT(CLAY_STRING("\""), rawTextConfig); + } + } else if (currentElement->children.length > 0) { + Clay__OpenElement(); + Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { .layout = { .padding = { .left = 8 } } }); + Clay__OpenElement(); + Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { .layout = { .padding = { .left = CLAY__DEBUGVIEW_INDENT_WIDTH }}, .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .left = 1 } }}); + Clay__OpenElement(); + Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { .layout = { .layoutDirection = CLAY_TOP_TO_BOTTOM } }); + } + + layoutData.rowCount++; + if (!(currentElement->isTextElement || (currentElementData && currentElementData->debugData.collapsed))) { + for (int32_t i = currentElement->children.length - 1; i >= 0; --i) { + Clay__int32_tArray_Add(&dfsBuffer, currentElement->children.elements[i]); + context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = false; // TODO needs to be ranged checked + } + } + } + } + + if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) { + Clay_ElementId collapseButtonId = Clay__HashString(CLAY_STRING("Clay__DebugView_CollapseElement"), 0); + for (int32_t i = (int)context->pointerOverIds.length - 1; i >= 0; i--) { + Clay_ElementId *elementId = Clay_ElementIdArray_Get(&context->pointerOverIds, i); + if (elementId->baseId == collapseButtonId.baseId) { + Clay_LayoutElementHashMapItem *highlightedItem = Clay__GetHashMapItem(elementId->offset); + highlightedItem->debugData.collapsed = !highlightedItem->debugData.collapsed; + break; + } + } + } + + if (highlightedElementId) { + CLAY(CLAY_ID("Clay__DebugView_ElementHighlight"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .floating = { .parentId = highlightedElementId, .zIndex = 32767, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID } }) { + CLAY(CLAY_ID("Clay__DebugView_ElementHighlightRectangle"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .backgroundColor = Clay__debugViewHighlightColor }) {} + } + } + return layoutData; +} + +void Clay__RenderDebugLayoutSizing(Clay_SizingAxis sizing, Clay_TextElementConfig infoTextConfig) { + Clay_String sizingLabel = CLAY_STRING("GROW"); + if (sizing.type == CLAY__SIZING_TYPE_FIT) { + sizingLabel = CLAY_STRING("FIT"); + } else if (sizing.type == CLAY__SIZING_TYPE_PERCENT) { + sizingLabel = CLAY_STRING("PERCENT"); + } else if (sizing.type == CLAY__SIZING_TYPE_FIXED) { + sizingLabel = CLAY_STRING("FIXED"); + } + CLAY_TEXT(sizingLabel, infoTextConfig); + if (sizing.type == CLAY__SIZING_TYPE_GROW || sizing.type == CLAY__SIZING_TYPE_FIT || sizing.type == CLAY__SIZING_TYPE_FIXED) { + CLAY_TEXT(CLAY_STRING("("), infoTextConfig); + if (sizing.size.minMax.min != 0) { + CLAY_TEXT(CLAY_STRING("min: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(sizing.size.minMax.min), infoTextConfig); + if (sizing.size.minMax.max != CLAY__MAXFLOAT) { + CLAY_TEXT(CLAY_STRING(", "), infoTextConfig); + } + } + if (sizing.size.minMax.max != CLAY__MAXFLOAT) { + CLAY_TEXT(CLAY_STRING("max: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(sizing.size.minMax.max), infoTextConfig); + } + CLAY_TEXT(CLAY_STRING(")"), infoTextConfig); + } else if (sizing.type == CLAY__SIZING_TYPE_PERCENT) { + CLAY_TEXT(CLAY_STRING("("), infoTextConfig); + CLAY_TEXT(Clay__IntToString(sizing.size.percent * 100), infoTextConfig); + CLAY_TEXT(CLAY_STRING("%)"), infoTextConfig); + } +} + +void Clay__DebugViewRenderElementConfigHeader(Clay_String elementId, Clay__DebugElementConfigType type) { + Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(type); + Clay_Color backgroundColor = config.color; + backgroundColor.a = 90; + CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = backgroundColor, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = config.color, .width = { 1, 1, 1, 1, 0 } } }) { + CLAY_TEXT(config.label, CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 })); + } +} + +void Clay__RenderDebugViewColor(Clay_Color color, Clay_TextElementConfig textConfig) { + CLAY_AUTO_ID({ .layout = { .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) { + CLAY_TEXT(CLAY_STRING("{ r: "), textConfig); + CLAY_TEXT(Clay__IntToString(color.r), textConfig); + CLAY_TEXT(CLAY_STRING(", g: "), textConfig); + CLAY_TEXT(Clay__IntToString(color.g), textConfig); + CLAY_TEXT(CLAY_STRING(", b: "), textConfig); + CLAY_TEXT(Clay__IntToString(color.b), textConfig); + CLAY_TEXT(CLAY_STRING(", a: "), textConfig); + CLAY_TEXT(Clay__IntToString(color.a), textConfig); + CLAY_TEXT(CLAY_STRING(" }"), textConfig); + CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_FIXED(10) } } }) {} + CLAY_AUTO_ID({ .layout = { .sizing = { CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 8), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 8)} }, .backgroundColor = color, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = CLAY__DEBUGVIEW_COLOR_4, .width = { 1, 1, 1, 1, 0 } } }) {} + } +} + +void Clay__RenderDebugViewCornerRadius(Clay_CornerRadius cornerRadius, Clay_TextElementConfig textConfig) { + CLAY_AUTO_ID({ .layout = { .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) { + CLAY_TEXT(CLAY_STRING("{ topLeft: "), textConfig); + CLAY_TEXT(Clay__IntToString(cornerRadius.topLeft), textConfig); + CLAY_TEXT(CLAY_STRING(", topRight: "), textConfig); + CLAY_TEXT(Clay__IntToString(cornerRadius.topRight), textConfig); + CLAY_TEXT(CLAY_STRING(", bottomLeft: "), textConfig); + CLAY_TEXT(Clay__IntToString(cornerRadius.bottomLeft), textConfig); + CLAY_TEXT(CLAY_STRING(", bottomRight: "), textConfig); + CLAY_TEXT(Clay__IntToString(cornerRadius.bottomRight), textConfig); + CLAY_TEXT(CLAY_STRING(" }"), textConfig); + } +} + +void HandleDebugViewCloseButtonInteraction(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData) { + Clay_Context* context = Clay_GetCurrentContext(); + (void) elementId; (void) pointerInfo; (void) userData; + if (pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) { + context->debugModeEnabled = false; + } +} + +void Clay__RenderDebugView(void) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay_ElementId closeButtonId = Clay__HashString(CLAY_STRING("Clay__DebugViewTopHeaderCloseButtonOuter"), 0); + if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) { + for (int32_t i = 0; i < context->pointerOverIds.length; ++i) { + Clay_ElementId *elementId = Clay_ElementIdArray_Get(&context->pointerOverIds, i); + if (elementId->id == closeButtonId.id) { + context->debugModeEnabled = false; + return; + } + } + } + + uint32_t initialRootsLength = context->layoutElementTreeRoots.length; + uint32_t initialElementsLength = context->layoutElements.length; + Clay_TextElementConfig infoTextConfig = CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE }); + Clay_TextElementConfig infoTitleConfig = CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE }); + Clay_ElementId scrollId = Clay__HashString(CLAY_STRING("Clay__DebugViewOuterScrollPane"), 0); + float scrollYOffset = 0; + bool pointerInDebugView = context->pointerInfo.position.y < context->layoutDimensions.height - 300; + for (int32_t i = 0; i < context->scrollContainerDatas.length; ++i) { + Clay__ScrollContainerDataInternal *scrollContainerData = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i); + if (scrollContainerData->elementId == scrollId.id) { + if (!context->externalScrollHandlingEnabled) { + scrollYOffset = scrollContainerData->scrollPosition.y; + } else { + pointerInDebugView = context->pointerInfo.position.y + scrollContainerData->scrollPosition.y < context->layoutDimensions.height - 300; + } + break; + } + } + int32_t highlightedRow = pointerInDebugView + ? (int32_t)((context->pointerInfo.position.y - scrollYOffset) / (float)CLAY__DEBUGVIEW_ROW_HEIGHT) - 1 + : -1; + if (context->pointerInfo.position.x < context->layoutDimensions.width - (float)Clay__debugViewWidth) { + highlightedRow = -1; + } + Clay__RenderDebugLayoutData layoutData = CLAY__DEFAULT_STRUCT; + CLAY(CLAY_ID("Clay__DebugView"), { + .layout = { .sizing = { CLAY_SIZING_FIXED((float)Clay__debugViewWidth) , CLAY_SIZING_FIXED(context->layoutDimensions.height) }, .layoutDirection = CLAY_TOP_TO_BOTTOM }, + .floating = { .zIndex = 32765, .attachPoints = { .element = CLAY_ATTACH_POINT_LEFT_CENTER, .parent = CLAY_ATTACH_POINT_RIGHT_CENTER }, .attachTo = CLAY_ATTACH_TO_ROOT, .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT }, + .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .bottom = 1 } } + }) { + CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_2 }) { + CLAY_TEXT(CLAY_STRING("Clay Debug Tools"), infoTextConfig); + CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_GROW(0) } } }) {} + // Close button + CLAY_AUTO_ID({ + .layout = { .sizing = {CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 10), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 10)}, .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER} }, + .backgroundColor = {217,91,67,80}, + .cornerRadius = CLAY_CORNER_RADIUS(4), + .border = { .color = { 217,91,67,255 }, .width = { 1, 1, 1, 1, 0 } }, + }) { + Clay_OnHover(HandleDebugViewCloseButtonInteraction, 0); + CLAY_TEXT(CLAY_STRING("x"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 })); + } + } + CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(1)} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_3 } ) {} + CLAY(scrollId, { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .clip = { .horizontal = true, .vertical = true, .childOffset = Clay_GetScrollOffset() } }) { + CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = ((initialElementsLength + initialRootsLength) & 1) == 0 ? CLAY__DEBUGVIEW_COLOR_2 : CLAY__DEBUGVIEW_COLOR_1 }) { + Clay_ElementId panelContentsId = Clay__HashString(CLAY_STRING("Clay__DebugViewPaneOuter"), 0); + // Element list + CLAY(panelContentsId, { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .floating = { .zIndex = 32766, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, .attachTo = CLAY_ATTACH_TO_PARENT, .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT } }) { + CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)}, .padding = { CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { + layoutData = Clay__RenderDebugLayoutElementsList((int32_t)initialRootsLength, highlightedRow); + } + } + float contentWidth = Clay__GetHashMapItem(panelContentsId.id)->layoutElement->dimensions.width; + CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(contentWidth) }, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {} + for (int32_t i = 0; i < layoutData.rowCount; i++) { + Clay_Color rowColor = (i & 1) == 0 ? CLAY__DEBUGVIEW_COLOR_2 : CLAY__DEBUGVIEW_COLOR_1; + if (i == layoutData.selectedElementRowIndex) { + rowColor = CLAY__DEBUGVIEW_COLOR_SELECTED_ROW; + } + if (i == highlightedRow) { + rowColor.r *= 1.25f; + rowColor.g *= 1.25f; + rowColor.b *= 1.25f; + } + CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = rowColor } ) {} + } + } + } + CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(1)} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_3 }) {} + Clay_LayoutElementHashMapItem *selectedItem = Clay__GetHashMapItem(context->debugSelectedElementId); + if (selectedItem->layoutElement) { + CLAY_AUTO_ID({ + .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(300)}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, + .backgroundColor = CLAY__DEBUGVIEW_COLOR_2 , + .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() }, + .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .betweenChildren = 1 } } + }) { + CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT + 8)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) { + CLAY_TEXT(CLAY_STRING("Element Configuration"), infoTextConfig); + CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_GROW(0) } } }) {} + if (selectedItem->elementId.stringId.length != 0) { + CLAY_TEXT(selectedItem->elementId.stringId, infoTitleConfig); + if (selectedItem->elementId.offset != 0) { + CLAY_TEXT(CLAY_STRING(" ("), infoTitleConfig); + CLAY_TEXT(Clay__IntToString(selectedItem->elementId.offset), infoTitleConfig); + CLAY_TEXT(CLAY_STRING(")"), infoTitleConfig); + } + } + } + Clay_Padding attributeConfigPadding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 8, 8}; + // Clay_LayoutConfig debug info + CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { + CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = { 200, 200, 200, 120 }, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = { 200, 200, 200, 255 }, .width = { 1, 1, 1, 1, 0 } } }) { + CLAY_TEXT(CLAY_STRING("Layout"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 })); + } + // .boundingBox + CLAY_TEXT(CLAY_STRING("Bounding Box"), infoTitleConfig); + CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { + CLAY_TEXT(CLAY_STRING("{ x: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.x), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", y: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.y), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", width: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.width), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", height: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.height), infoTextConfig); + CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig); + } + if (!selectedItem->layoutElement->isTextElement) { + // .layoutDirection + CLAY_TEXT(CLAY_STRING("Layout Direction"), infoTitleConfig); + Clay_LayoutConfig *layoutConfig = &selectedItem->layoutElement->config.layout; + CLAY_TEXT(layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM ? CLAY_STRING("TOP_TO_BOTTOM") : CLAY_STRING("LEFT_TO_RIGHT"), infoTextConfig); + // .sizing + CLAY_TEXT(CLAY_STRING("Sizing"), infoTitleConfig); + CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { + CLAY_TEXT(CLAY_STRING("width: "), infoTextConfig); + Clay__RenderDebugLayoutSizing(layoutConfig->sizing.width, infoTextConfig); + } + CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { + CLAY_TEXT(CLAY_STRING("height: "), infoTextConfig); + Clay__RenderDebugLayoutSizing(layoutConfig->sizing.height, infoTextConfig); + } + // .padding + CLAY_TEXT(CLAY_STRING("Padding"), infoTitleConfig); + CLAY(CLAY_ID("Clay__DebugViewElementInfoPadding"), { }) { + CLAY_TEXT(CLAY_STRING("{ left: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(layoutConfig->padding.left), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", right: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(layoutConfig->padding.right), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", top: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(layoutConfig->padding.top), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", bottom: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(layoutConfig->padding.bottom), infoTextConfig); + CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig); + } + // .childGap + CLAY_TEXT(CLAY_STRING("Child Gap"), infoTitleConfig); + CLAY_TEXT(Clay__IntToString(layoutConfig->childGap), infoTextConfig); + // .childAlignment + CLAY_TEXT(CLAY_STRING("Child Alignment"), infoTitleConfig); + CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { + CLAY_TEXT(CLAY_STRING("{ x: "), infoTextConfig); + Clay_String alignX = CLAY_STRING("LEFT"); + if (layoutConfig->childAlignment.x == CLAY_ALIGN_X_CENTER) { + alignX = CLAY_STRING("CENTER"); + } else if (layoutConfig->childAlignment.x == CLAY_ALIGN_X_RIGHT) { + alignX = CLAY_STRING("RIGHT"); + } + CLAY_TEXT(alignX, infoTextConfig); + CLAY_TEXT(CLAY_STRING(", y: "), infoTextConfig); + Clay_String alignY = CLAY_STRING("TOP"); + if (layoutConfig->childAlignment.y == CLAY_ALIGN_Y_CENTER) { + alignY = CLAY_STRING("CENTER"); + } else if (layoutConfig->childAlignment.y == CLAY_ALIGN_Y_BOTTOM) { + alignY = CLAY_STRING("BOTTOM"); + } + CLAY_TEXT(alignY, infoTextConfig); + CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig); + } + } + } + if (selectedItem->layoutElement->isTextElement) { + Clay_TextElementConfig *textConfig = &selectedItem->layoutElement->textConfig; + CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { + Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_TEXT); + // .fontSize + CLAY_TEXT(CLAY_STRING("Font Size"), infoTitleConfig); + CLAY_TEXT(Clay__IntToString(textConfig->fontSize), infoTextConfig); + // .fontId + CLAY_TEXT(CLAY_STRING("Font ID"), infoTitleConfig); + CLAY_TEXT(Clay__IntToString(textConfig->fontId), infoTextConfig); + // .lineHeight + CLAY_TEXT(CLAY_STRING("Line Height"), infoTitleConfig); + CLAY_TEXT(textConfig->lineHeight == 0 ? CLAY_STRING("auto") : Clay__IntToString(textConfig->lineHeight), infoTextConfig); + // .letterSpacing + CLAY_TEXT(CLAY_STRING("Letter Spacing"), infoTitleConfig); + CLAY_TEXT(Clay__IntToString(textConfig->letterSpacing), infoTextConfig); + // .wrapMode + CLAY_TEXT(CLAY_STRING("Wrap Mode"), infoTitleConfig); + Clay_String wrapMode = CLAY_STRING("WORDS"); + if (textConfig->wrapMode == CLAY_TEXT_WRAP_NONE) { + wrapMode = CLAY_STRING("NONE"); + } else if (textConfig->wrapMode == CLAY_TEXT_WRAP_NEWLINES) { + wrapMode = CLAY_STRING("NEWLINES"); + } + CLAY_TEXT(wrapMode, infoTextConfig); + // .textAlignment + CLAY_TEXT(CLAY_STRING("Text Alignment"), infoTitleConfig); + Clay_String textAlignment = CLAY_STRING("LEFT"); + if (textConfig->textAlignment == CLAY_TEXT_ALIGN_CENTER) { + textAlignment = CLAY_STRING("CENTER"); + } else if (textConfig->textAlignment == CLAY_TEXT_ALIGN_RIGHT) { + textAlignment = CLAY_STRING("RIGHT"); + } + CLAY_TEXT(textAlignment, infoTextConfig); + // .textColor + CLAY_TEXT(CLAY_STRING("Text Color"), infoTitleConfig); + Clay__RenderDebugViewColor(textConfig->textColor, infoTextConfig); + } + } else { + CLAY(CLAY_ID("Clay__DebugViewElementInfoSharedBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { + Clay__DebugElementConfigTypeLabelConfig labelConfig = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR); + Clay_Color backgroundColor = labelConfig.color; + backgroundColor.a = 90; + CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = backgroundColor, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = labelConfig.color, .width = { 1, 1, 1, 1, 0 } } }) { + CLAY_TEXT(CLAY_STRING("Color & Radius"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 })); + } + // .backgroundColor + if (selectedItem->layoutElement->config.backgroundColor.a > 0) { + CLAY_TEXT(CLAY_STRING("Background Color"), infoTitleConfig); + Clay__RenderDebugViewColor(selectedItem->layoutElement->config.backgroundColor, infoTextConfig); + } + // .cornerRadius + if (!Clay__MemCmp((const char*)&selectedItem->layoutElement->config.cornerRadius, (const char*)&Clay__CornerRadius_DEFAULT, sizeof(Clay_CornerRadius))) { + CLAY_TEXT(CLAY_STRING("Corner Radius"), infoTitleConfig); + Clay__RenderDebugViewCornerRadius(selectedItem->layoutElement->config.cornerRadius, infoTextConfig); + } + // .overlayColor + if (selectedItem->layoutElement->config.overlayColor.a > 0) { + CLAY_TEXT(CLAY_STRING("Overlay Color"), infoTitleConfig); + Clay__RenderDebugViewColor(selectedItem->layoutElement->config.overlayColor, infoTextConfig); + } + } + if (selectedItem->layoutElement->config.aspectRatio.aspectRatio > 0) { + Clay_AspectRatioElementConfig *aspectRatioConfig = &selectedItem->layoutElement->config.aspectRatio; + CLAY(CLAY_ID("Clay__DebugViewElementInfoAspectRatioBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { + Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_ASPECT); + CLAY_TEXT(CLAY_STRING("Aspect Ratio"), infoTitleConfig); + // Aspect Ratio + CLAY(CLAY_ID("Clay__DebugViewElementInfoAspectRatio"), { }) { + CLAY_TEXT(Clay__IntToString(aspectRatioConfig->aspectRatio), infoTextConfig); + CLAY_TEXT(CLAY_STRING("."), infoTextConfig); + float frac = aspectRatioConfig->aspectRatio - (int)(aspectRatioConfig->aspectRatio); + frac *= 100; + if ((int)frac < 10) { + CLAY_TEXT(CLAY_STRING("0"), infoTextConfig); + } + CLAY_TEXT(Clay__IntToString(frac), infoTextConfig); + } + } + } + if (selectedItem->layoutElement->config.image.imageData) { + Clay_ImageElementConfig *imageConfig = &selectedItem->layoutElement->config.image; + Clay_AspectRatioElementConfig aspectConfig = { 1 }; + if (selectedItem->layoutElement->config.aspectRatio.aspectRatio > 0) { + aspectConfig = selectedItem->layoutElement->config.aspectRatio; + } + CLAY(CLAY_ID("Clay__DebugViewElementInfoImageBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { + Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_IMAGE); + // Image Preview + CLAY_TEXT(CLAY_STRING("Preview"), infoTitleConfig); + CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_GROW(64, 128), .height = CLAY_SIZING_GROW(64, 128) }}, .aspectRatio = aspectConfig, .image = *imageConfig }) {} + } + } + if (selectedItem->layoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) { + Clay_FloatingElementConfig* floatingConfig = &selectedItem->layoutElement->config.floating; + CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { + Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_FLOATING); + // .offset + CLAY_TEXT(CLAY_STRING("Offset"), infoTitleConfig); + CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { + CLAY_TEXT(CLAY_STRING("{ x: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(floatingConfig->offset.x), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", y: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(floatingConfig->offset.y), infoTextConfig); + CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig); + } + // .expand + CLAY_TEXT(CLAY_STRING("Expand"), infoTitleConfig); + CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { + CLAY_TEXT(CLAY_STRING("{ width: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(floatingConfig->expand.width), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", height: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(floatingConfig->expand.height), infoTextConfig); + CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig); + } + // .zIndex + CLAY_TEXT(CLAY_STRING("z-index"), infoTitleConfig); + CLAY_TEXT(Clay__IntToString(floatingConfig->zIndex), infoTextConfig); + // .parentId + CLAY_TEXT(CLAY_STRING("Parent"), infoTitleConfig); + Clay_LayoutElementHashMapItem *hashItem = Clay__GetHashMapItem(floatingConfig->parentId); + CLAY_TEXT(hashItem->elementId.stringId, infoTextConfig); + // .attachPoints + CLAY_TEXT(CLAY_STRING("Attach Points"), infoTitleConfig); + CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { + CLAY_TEXT(CLAY_STRING("{ element: "), infoTextConfig); + Clay_String attachPointElement = CLAY_STRING("LEFT_TOP"); + if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_LEFT_CENTER) { + attachPointElement = CLAY_STRING("LEFT_CENTER"); + } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_LEFT_BOTTOM) { + attachPointElement = CLAY_STRING("LEFT_BOTTOM"); + } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_CENTER_TOP) { + attachPointElement = CLAY_STRING("CENTER_TOP"); + } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_CENTER_CENTER) { + attachPointElement = CLAY_STRING("CENTER_CENTER"); + } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_CENTER_BOTTOM) { + attachPointElement = CLAY_STRING("CENTER_BOTTOM"); + } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_RIGHT_TOP) { + attachPointElement = CLAY_STRING("RIGHT_TOP"); + } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_RIGHT_CENTER) { + attachPointElement = CLAY_STRING("RIGHT_CENTER"); + } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_RIGHT_BOTTOM) { + attachPointElement = CLAY_STRING("RIGHT_BOTTOM"); + } + CLAY_TEXT(attachPointElement, infoTextConfig); + Clay_String attachPointParent = CLAY_STRING("LEFT_TOP"); + if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_LEFT_CENTER) { + attachPointParent = CLAY_STRING("LEFT_CENTER"); + } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_LEFT_BOTTOM) { + attachPointParent = CLAY_STRING("LEFT_BOTTOM"); + } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_CENTER_TOP) { + attachPointParent = CLAY_STRING("CENTER_TOP"); + } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_CENTER_CENTER) { + attachPointParent = CLAY_STRING("CENTER_CENTER"); + } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_CENTER_BOTTOM) { + attachPointParent = CLAY_STRING("CENTER_BOTTOM"); + } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_RIGHT_TOP) { + attachPointParent = CLAY_STRING("RIGHT_TOP"); + } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_RIGHT_CENTER) { + attachPointParent = CLAY_STRING("RIGHT_CENTER"); + } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_RIGHT_BOTTOM) { + attachPointParent = CLAY_STRING("RIGHT_BOTTOM"); + } + CLAY_TEXT(CLAY_STRING(", parent: "), infoTextConfig); + CLAY_TEXT(attachPointParent, infoTextConfig); + CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig); + } + // .pointerCaptureMode + CLAY_TEXT(CLAY_STRING("Pointer Capture Mode"), infoTitleConfig); + Clay_String pointerCaptureMode = CLAY_STRING("NONE"); + if (floatingConfig->pointerCaptureMode == CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH) { + pointerCaptureMode = CLAY_STRING("PASSTHROUGH"); + } + CLAY_TEXT(pointerCaptureMode, infoTextConfig); + // .attachTo + CLAY_TEXT(CLAY_STRING("Attach To"), infoTitleConfig); + Clay_String attachTo = CLAY_STRING("NONE"); + if (floatingConfig->attachTo == CLAY_ATTACH_TO_PARENT) { + attachTo = CLAY_STRING("PARENT"); + } else if (floatingConfig->attachTo == CLAY_ATTACH_TO_ELEMENT_WITH_ID) { + attachTo = CLAY_STRING("ELEMENT_WITH_ID"); + } else if (floatingConfig->attachTo == CLAY_ATTACH_TO_ROOT) { + attachTo = CLAY_STRING("ROOT"); + } + CLAY_TEXT(attachTo, infoTextConfig); + // .clipTo + CLAY_TEXT(CLAY_STRING("Clip To"), infoTitleConfig); + Clay_String clipTo = CLAY_STRING("ATTACHED_PARENT"); + if (floatingConfig->clipTo == CLAY_CLIP_TO_NONE) { + clipTo = CLAY_STRING("NONE"); + } + CLAY_TEXT(clipTo, infoTextConfig); + } + } + Clay_ClipElementConfig *clipConfig = &selectedItem->layoutElement->config.clip; + if (clipConfig->horizontal || clipConfig->vertical) { + CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { + Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_CLIP); + // .vertical + CLAY_TEXT(CLAY_STRING("Vertical"), infoTitleConfig); + CLAY_TEXT(clipConfig->vertical ? CLAY_STRING("true") : CLAY_STRING("false") , infoTextConfig); + // .horizontal + CLAY_TEXT(CLAY_STRING("Horizontal"), infoTitleConfig); + CLAY_TEXT(clipConfig->horizontal ? CLAY_STRING("true") : CLAY_STRING("false") , infoTextConfig); + } + } + Clay_BorderElementConfig *borderConfig = &selectedItem->layoutElement->config.border; + if (Clay__BorderHasAnyWidth(borderConfig)) { + CLAY(CLAY_ID("Clay__DebugViewElementInfoBorderBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) { + Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_BORDER); + CLAY_TEXT(CLAY_STRING("Border Widths"), infoTitleConfig); + CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { + CLAY_TEXT(CLAY_STRING("{ left: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(borderConfig->width.left), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", right: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(borderConfig->width.right), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", top: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(borderConfig->width.top), infoTextConfig); + CLAY_TEXT(CLAY_STRING(", bottom: "), infoTextConfig); + CLAY_TEXT(Clay__IntToString(borderConfig->width.bottom), infoTextConfig); + CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig); + } + // .textColor + CLAY_TEXT(CLAY_STRING("Border Color"), infoTitleConfig); + Clay__RenderDebugViewColor(borderConfig->color, infoTextConfig); + } + } + } + } + } else { + CLAY(CLAY_ID("Clay__DebugViewWarningsScrollPane"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(300)}, .childGap = 6, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_2, .clip = { .horizontal = true, .vertical = true, .childOffset = Clay_GetScrollOffset() } }) { + Clay_TextElementConfig warningConfig = CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE }); + CLAY(CLAY_ID("Clay__DebugViewWarningItemHeader"), { .layout = { .sizing = {.height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childGap = 8, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) { + CLAY_TEXT(CLAY_STRING("Warnings"), warningConfig); + } + CLAY(CLAY_ID("Clay__DebugViewWarningsTopBorder"), { .layout = { .sizing = { .width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(1)} }, .backgroundColor = {200, 200, 200, 255} }) {} + int32_t previousWarningsLength = context->warnings.length; + for (int32_t i = 0; i < previousWarningsLength; i++) { + Clay__Warning warning = context->warnings.internalArray[i]; + CLAY(CLAY_IDI("Clay__DebugViewWarningItem", i), { .layout = { .sizing = {.height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childGap = 8, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) { + CLAY_TEXT(warning.baseMessage, warningConfig); + if (warning.dynamicMessage.length > 0) { + CLAY_TEXT(warning.dynamicMessage, warningConfig); + } + } + } + } + } + } +} +#pragma endregion + +uint32_t Clay__debugViewWidth = 400; +Clay_Color Clay__debugViewHighlightColor = { 168, 66, 28, 100 }; + +Clay__WarningArray Clay__WarningArray_Allocate_Arena(int32_t capacity, Clay_Arena *arena) { + size_t totalSizeBytes = capacity * sizeof(Clay_String); + Clay__WarningArray array = {.capacity = capacity, .length = 0}; + uintptr_t nextAllocOffset = arena->nextAllocation + (64 - (arena->nextAllocation % 64)); + if (nextAllocOffset + totalSizeBytes <= arena->capacity) { + array.internalArray = (Clay__Warning*)((uintptr_t)arena->memory + (uintptr_t)nextAllocOffset); + arena->nextAllocation = nextAllocOffset + totalSizeBytes; + } + else { + Clay__currentContext->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED, + .errorText = CLAY_STRING("Clay attempted to allocate memory in its arena, but ran out of capacity. Try increasing the capacity of the arena passed to Clay_Initialize()"), + .userData = Clay__currentContext->errorHandler.userData }); + } + return array; +} + +Clay__Warning *Clay__WarningArray_Add(Clay__WarningArray *array, Clay__Warning item) +{ + if (array->length < array->capacity) { + array->internalArray[array->length++] = item; + return &array->internalArray[array->length - 1]; + } + return &CLAY__WARNING_DEFAULT; +} + +void* Clay__Array_Allocate_Arena(int32_t capacity, uint32_t itemSize, Clay_Arena *arena) +{ + size_t totalSizeBytes = capacity * itemSize; + uintptr_t nextAllocOffset = arena->nextAllocation + ((64 - (arena->nextAllocation % 64)) & 63); + if (nextAllocOffset + totalSizeBytes <= arena->capacity) { + arena->nextAllocation = nextAllocOffset + totalSizeBytes; + return (void*)((uintptr_t)arena->memory + (uintptr_t)nextAllocOffset); + } + else { + Clay__currentContext->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED, + .errorText = CLAY_STRING("Clay attempted to allocate memory in its arena, but ran out of capacity. Try increasing the capacity of the arena passed to Clay_Initialize()"), + .userData = Clay__currentContext->errorHandler.userData }); + } + return CLAY__NULL; +} + +bool Clay__Array_RangeCheck(int32_t index, int32_t length) +{ + if (index < length && index >= 0) { + return true; + } + Clay_Context* context = Clay_GetCurrentContext(); + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_INTERNAL_ERROR, + .errorText = CLAY_STRING("Clay attempted to make an out of bounds array access. This is an internal error and is likely a bug."), + .userData = context->errorHandler.userData }); + return false; +} + +bool Clay__Array_AddCapacityCheck(int32_t length, int32_t capacity) +{ + if (length < capacity) { + return true; + } + Clay_Context* context = Clay_GetCurrentContext(); + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_INTERNAL_ERROR, + .errorText = CLAY_STRING("Clay attempted to make an out of bounds array access. This is an internal error and is likely a bug."), + .userData = context->errorHandler.userData }); + return false; +} + +// PUBLIC API FROM HERE --------------------------------------- + +CLAY_WASM_EXPORT("Clay_MinMemorySize") +uint32_t Clay_MinMemorySize(void) { + Clay_Context fakeContext = { + .maxElementCount = Clay__defaultMaxElementCount, + .maxMeasureTextCacheWordCount = Clay__defaultMaxMeasureTextWordCacheCount, + .internalArena = { + .capacity = SIZE_MAX, + .memory = NULL, + } + }; + Clay_Context* currentContext = Clay_GetCurrentContext(); + if (currentContext) { + fakeContext.maxElementCount = currentContext->maxElementCount; + fakeContext.maxMeasureTextCacheWordCount = currentContext->maxMeasureTextCacheWordCount; + } + // Reserve space in the arena for the context, important for calculating min memory size correctly + Clay__Context_Allocate_Arena(&fakeContext.internalArena); + Clay__InitializePersistentMemory(&fakeContext); + Clay__InitializeEphemeralMemory(&fakeContext); + return (uint32_t)fakeContext.internalArena.nextAllocation + 128; +} + +CLAY_WASM_EXPORT("Clay_CreateArenaWithCapacityAndMemory") +Clay_Arena Clay_CreateArenaWithCapacityAndMemory(size_t capacity, void *memory) { + Clay_Arena arena = { + .capacity = capacity, + .memory = (char *)memory + }; + return arena; +} + +#ifndef CLAY_WASM +void Clay_SetMeasureTextFunction(Clay_Dimensions (*measureTextFunction)(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData), void *userData) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay__MeasureText = measureTextFunction; + context->measureTextUserData = userData; +} +void Clay_SetQueryScrollOffsetFunction(Clay_Vector2 (*queryScrollOffsetFunction)(uint32_t elementId, void *userData), void *userData) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay__QueryScrollOffset = queryScrollOffsetFunction; + context->queryScrollOffsetUserData = userData; +} +#endif + +CLAY_WASM_EXPORT("Clay_SetLayoutDimensions") +void Clay_SetLayoutDimensions(Clay_Dimensions dimensions) { + Clay_Context* context = Clay_GetCurrentContext(); + context->rootResizedLastFrame = !Clay__FloatEqual(context->layoutDimensions.width, dimensions.width) || !Clay__FloatEqual(context->layoutDimensions.height, dimensions.height); + context->layoutDimensions = dimensions; +} + +CLAY_WASM_EXPORT("Clay_SetLayoutDimensions") +Clay_Dimensions Clay_GetLayoutDimensions() { + Clay_Context* context = Clay_GetCurrentContext(); + return context->layoutDimensions; +} + +CLAY_WASM_EXPORT("Clay_SetPointerState") +void Clay_SetPointerState(Clay_Vector2 position, bool isPointerDown) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->booleanWarnings.maxElementsExceeded) { + return; + } + context->pointerInfo.position = position; + context->pointerOverIds.length = 0; + Clay__int32_tArray dfsBuffer = context->layoutElementChildrenBuffer; + for (int32_t rootIndex = context->layoutElementTreeRoots.length - 1; rootIndex >= 0; --rootIndex) { + dfsBuffer.length = 0; + Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex); + Clay__int32_tArray_Add(&dfsBuffer, (int32_t)root->layoutElementIndex); + context->treeNodeVisited.internalArray[0] = false; + bool found = false; + bool skipTree = false; + while (dfsBuffer.length > 0) { + if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) { + dfsBuffer.length--; + continue; + } + context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true; + Clay_LayoutElement *currentElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&dfsBuffer, (int)dfsBuffer.length - 1)); + + Clay_LayoutElementHashMapItem *mapItem = Clay__GetHashMapItem(currentElement->id); // TODO think of a way around this, maybe the fact that it's essentially a binary tree limits the cost, but the worst case is not great + int32_t clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, (int32_t)(currentElement - context->layoutElements.internalArray)); + Clay_LayoutElementHashMapItem *clipItem = Clay__GetHashMapItem(clipElementId); + // This check skips mouse interactions for elements that are currently "exit transitioning" + if (mapItem && mapItem->generation > context->generation) { + // Conditionally skip mouse interactions on non-exit transitions, based on user config + if (!currentElement->isTextElement && currentElement->config.transition.handler) { + for (int I = 0; I < context->transitionDatas.length; ++I) { + Clay__TransitionDataInternal* data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, I); + if (data->elementId == currentElement->id) { + if (currentElement->config.transition.interactionHandling == CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION) { + if (data->state == CLAY_TRANSITION_STATE_EXITING || data->state == CLAY_TRANSITION_STATE_ENTERING || ((data->activeProperties & CLAY_TRANSITION_PROPERTY_POSITION) && data->state == CLAY_TRANSITION_STATE_TRANSITIONING)) { + skipTree = true; + } + } else if (currentElement->config.transition.interactionHandling == CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION) { + if (data->state == CLAY_TRANSITION_STATE_EXITING) { + skipTree = true; + } + } + } + } + } + + if (skipTree) { + dfsBuffer.length--; + continue; + } + + Clay_BoundingBox elementBox = mapItem->boundingBox; + elementBox.x -= root->pointerOffset.x; + elementBox.y -= root->pointerOffset.y; + if ((Clay__PointIsInsideRect(position, elementBox)) && (clipElementId == 0 || (Clay__PointIsInsideRect(position, clipItem->boundingBox)) || context->externalScrollHandlingEnabled)) { + if (!skipTree) { + if (mapItem->onHoverFunction) { + mapItem->onHoverFunction(mapItem->elementId, context->pointerInfo, mapItem->hoverFunctionUserData); + } + Clay_ElementIdArray_Add(&context->pointerOverIds, mapItem->elementId); + } + found = true; + } + + for (int32_t i = currentElement->children.length - 1; i >= 0; --i) { + Clay__int32_tArray_Add(&dfsBuffer, currentElement->children.elements[i]); + context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = false; // TODO needs to be ranged checked + } + } else { + dfsBuffer.length--; + } + } + + Clay_LayoutElement *rootElement = Clay_LayoutElementArray_Get(&context->layoutElements, root->layoutElementIndex); + if (found && rootElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE && rootElement->config.floating.pointerCaptureMode == CLAY_POINTER_CAPTURE_MODE_CAPTURE) { + break; + } + } + + if (isPointerDown) { + if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) { + context->pointerInfo.state = CLAY_POINTER_DATA_PRESSED; + } else if (context->pointerInfo.state != CLAY_POINTER_DATA_PRESSED) { + context->pointerInfo.state = CLAY_POINTER_DATA_PRESSED_THIS_FRAME; + } + } else { + if (context->pointerInfo.state == CLAY_POINTER_DATA_RELEASED_THIS_FRAME) { + context->pointerInfo.state = CLAY_POINTER_DATA_RELEASED; + } else if (context->pointerInfo.state != CLAY_POINTER_DATA_RELEASED) { + context->pointerInfo.state = CLAY_POINTER_DATA_RELEASED_THIS_FRAME; + } + } +} + +CLAY_WASM_EXPORT("Clay_GetPointerState") +CLAY_DLL_EXPORT Clay_PointerData Clay_GetPointerState(void) { + return Clay_GetCurrentContext()->pointerInfo; +} + +CLAY_WASM_EXPORT("Clay_Initialize") +Clay_Context* Clay_Initialize(Clay_Arena arena, Clay_Dimensions layoutDimensions, Clay_ErrorHandler errorHandler) { + // Cacheline align memory passed in + uintptr_t baseOffset = 64 - ((uintptr_t)arena.memory % 64); + baseOffset = baseOffset == 64 ? 0 : baseOffset; + arena.memory += baseOffset; + Clay_Context *context = Clay__Context_Allocate_Arena(&arena); + if (context == NULL) return NULL; + // DEFAULTS + Clay_Context *oldContext = Clay_GetCurrentContext(); + *context = CLAY__INIT(Clay_Context) { + .maxElementCount = oldContext ? oldContext->maxElementCount : Clay__defaultMaxElementCount, + .maxMeasureTextCacheWordCount = oldContext ? oldContext->maxMeasureTextCacheWordCount : Clay__defaultMaxMeasureTextWordCacheCount, + .errorHandler = errorHandler.errorHandlerFunction ? errorHandler : CLAY__INIT(Clay_ErrorHandler) { Clay__ErrorHandlerFunctionDefault, 0 }, + .layoutDimensions = layoutDimensions, + .internalArena = arena, + }; + Clay_SetCurrentContext(context); + Clay__InitializePersistentMemory(context); + Clay__InitializeEphemeralMemory(context); + for (int32_t i = 0; i < context->layoutElementsHashMap.capacity; ++i) { + context->layoutElementsHashMap.internalArray[i] = -1; + } + for (int32_t i = 0; i < context->measureTextHashMap.capacity; ++i) { + context->measureTextHashMap.internalArray[i] = 0; + } + context->measureTextHashMapInternal.length = 1; // Reserve the 0 value to mean "no next element" + context->layoutDimensions = layoutDimensions; + return context; +} + +CLAY_WASM_EXPORT("Clay_GetCurrentContext") +Clay_Context* Clay_GetCurrentContext(void) { + return Clay__currentContext; +} + +CLAY_WASM_EXPORT("Clay_SetCurrentContext") +void Clay_SetCurrentContext(Clay_Context* context) { + Clay__currentContext = context; +} + +CLAY_WASM_EXPORT("Clay_GetScrollOffset") +Clay_Vector2 Clay_GetScrollOffset(void) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->booleanWarnings.maxElementsExceeded) { + return CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT; + } + Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement(); + for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) { + Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i); + if (mapping->elementId == openLayoutElement->id) { + return mapping->scrollPosition; + } + } + return CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT; +} + +CLAY_WASM_EXPORT("Clay_UpdateScrollContainers") +void Clay_UpdateScrollContainers(bool enableDragScrolling, Clay_Vector2 scrollDelta, float deltaTime) { + Clay_Context* context = Clay_GetCurrentContext(); + bool isPointerActive = enableDragScrolling && (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED || context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME); + // Don't apply scroll events to ancestors of the inner element + int32_t highestPriorityElementIndex = -1; + Clay__ScrollContainerDataInternal *highestPriorityScrollData = CLAY__NULL; + for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) { + Clay__ScrollContainerDataInternal *scrollData = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i); + if (!scrollData->openThisFrame) { + Clay__ScrollContainerDataInternalArray_RemoveSwapback(&context->scrollContainerDatas, i); + continue; + } + scrollData->openThisFrame = false; + Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(scrollData->elementId); + // Element isn't rendered this frame but scroll offset has been retained + if (!hashMapItem) { + Clay__ScrollContainerDataInternalArray_RemoveSwapback(&context->scrollContainerDatas, i); + continue; + } + + // Touch / click is released + if (!isPointerActive && scrollData->pointerScrollActive) { + float xDiff = scrollData->scrollPosition.x - scrollData->scrollOrigin.x; + if (xDiff < -10 || xDiff > 10) { + scrollData->scrollMomentum.x = (scrollData->scrollPosition.x - scrollData->scrollOrigin.x) / (scrollData->momentumTime * 25); + } + float yDiff = scrollData->scrollPosition.y - scrollData->scrollOrigin.y; + if (yDiff < -10 || yDiff > 10) { + scrollData->scrollMomentum.y = (scrollData->scrollPosition.y - scrollData->scrollOrigin.y) / (scrollData->momentumTime * 25); + } + scrollData->pointerScrollActive = false; + + scrollData->pointerOrigin = CLAY__INIT(Clay_Vector2){0,0}; + scrollData->scrollOrigin = CLAY__INIT(Clay_Vector2){0,0}; + scrollData->momentumTime = 0; + } + + // Apply existing momentum + scrollData->scrollPosition.x += scrollData->scrollMomentum.x; + scrollData->scrollMomentum.x *= 0.95f; + bool scrollOccurred = scrollDelta.x != 0 || scrollDelta.y != 0; + if ((scrollData->scrollMomentum.x > -0.1f && scrollData->scrollMomentum.x < 0.1f) || scrollOccurred) { + scrollData->scrollMomentum.x = 0; + } + scrollData->scrollPosition.x = CLAY__MIN(CLAY__MAX(scrollData->scrollPosition.x, -(CLAY__MAX(scrollData->contentSize.width - scrollData->layoutElement->dimensions.width, 0))), 0); + + scrollData->scrollPosition.y += scrollData->scrollMomentum.y; + scrollData->scrollMomentum.y *= 0.95f; + if ((scrollData->scrollMomentum.y > -0.1f && scrollData->scrollMomentum.y < 0.1f) || scrollOccurred) { + scrollData->scrollMomentum.y = 0; + } + scrollData->scrollPosition.y = CLAY__MIN(CLAY__MAX(scrollData->scrollPosition.y, -(CLAY__MAX(scrollData->contentSize.height - scrollData->layoutElement->dimensions.height, 0))), 0); + + for (int32_t j = 0; j < context->pointerOverIds.length; ++j) { // TODO n & m are small here but this being n*m gives me the creeps + if (scrollData->layoutElement->id == Clay_ElementIdArray_Get(&context->pointerOverIds, j)->id) { + highestPriorityElementIndex = j; + highestPriorityScrollData = scrollData; + } + } + } + + if (highestPriorityElementIndex > -1 && highestPriorityScrollData) { + Clay_LayoutElement *scrollElement = highestPriorityScrollData->layoutElement; + Clay_ClipElementConfig *clipConfig = &scrollElement->config.clip; + bool canScrollVertically = clipConfig->vertical && highestPriorityScrollData->contentSize.height > scrollElement->dimensions.height; + bool canScrollHorizontally = clipConfig->horizontal && highestPriorityScrollData->contentSize.width > scrollElement->dimensions.width; + // Handle wheel scroll + if (canScrollVertically) { + highestPriorityScrollData->scrollPosition.y = highestPriorityScrollData->scrollPosition.y + scrollDelta.y * 10; + } + if (canScrollHorizontally) { + highestPriorityScrollData->scrollPosition.x = highestPriorityScrollData->scrollPosition.x + scrollDelta.x * 10; + } + // Handle click / touch scroll + if (isPointerActive) { + highestPriorityScrollData->scrollMomentum = CLAY__INIT(Clay_Vector2)CLAY__DEFAULT_STRUCT; + if (!highestPriorityScrollData->pointerScrollActive) { + highestPriorityScrollData->pointerOrigin = context->pointerInfo.position; + highestPriorityScrollData->scrollOrigin = highestPriorityScrollData->scrollPosition; + highestPriorityScrollData->pointerScrollActive = true; + } else { + float scrollDeltaX = 0, scrollDeltaY = 0; + if (canScrollHorizontally) { + float oldXScrollPosition = highestPriorityScrollData->scrollPosition.x; + highestPriorityScrollData->scrollPosition.x = highestPriorityScrollData->scrollOrigin.x + (context->pointerInfo.position.x - highestPriorityScrollData->pointerOrigin.x); + highestPriorityScrollData->scrollPosition.x = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.x, 0), -(highestPriorityScrollData->contentSize.width - highestPriorityScrollData->boundingBox.width)); + scrollDeltaX = highestPriorityScrollData->scrollPosition.x - oldXScrollPosition; + } + if (canScrollVertically) { + float oldYScrollPosition = highestPriorityScrollData->scrollPosition.y; + highestPriorityScrollData->scrollPosition.y = highestPriorityScrollData->scrollOrigin.y + (context->pointerInfo.position.y - highestPriorityScrollData->pointerOrigin.y); + highestPriorityScrollData->scrollPosition.y = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.y, 0), -(highestPriorityScrollData->contentSize.height - highestPriorityScrollData->boundingBox.height)); + scrollDeltaY = highestPriorityScrollData->scrollPosition.y - oldYScrollPosition; + } + if (scrollDeltaX > -0.1f && scrollDeltaX < 0.1f && scrollDeltaY > -0.1f && scrollDeltaY < 0.1f && highestPriorityScrollData->momentumTime > 0.15f) { + highestPriorityScrollData->momentumTime = 0; + highestPriorityScrollData->pointerOrigin = context->pointerInfo.position; + highestPriorityScrollData->scrollOrigin = highestPriorityScrollData->scrollPosition; + } else { + highestPriorityScrollData->momentumTime += deltaTime; + } + } + } + // Clamp any changes to scroll position to the maximum size of the contents + if (canScrollVertically) { + highestPriorityScrollData->scrollPosition.y = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.y, 0), -(highestPriorityScrollData->contentSize.height - scrollElement->dimensions.height)); + } + if (canScrollHorizontally) { + highestPriorityScrollData->scrollPosition.x = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.x, 0), -(highestPriorityScrollData->contentSize.width - scrollElement->dimensions.width)); + } + } +} + +CLAY_WASM_EXPORT("Clay_BeginLayout") +void Clay_BeginLayout(void) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay__InitializeEphemeralMemory(context); + context->generation++; + context->dynamicElementIndex = 0; + // Set up the root container that covers the entire window + Clay_Dimensions rootDimensions = {context->layoutDimensions.width, context->layoutDimensions.height}; + if (context->debugModeEnabled) { + rootDimensions.width -= (float)Clay__debugViewWidth; + } + context->booleanWarnings = CLAY__INIT(Clay_BooleanWarnings) CLAY__DEFAULT_STRUCT; + Clay__OpenElementWithId(CLAY_ID("Clay__RootContainer")); + Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { + .layout = { .sizing = {CLAY_SIZING_FIXED((rootDimensions.width)), CLAY_SIZING_FIXED(rootDimensions.height)} } + }); + Clay__int32_tArray_Add(&context->openLayoutElementStack, 0); + Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) { .layoutElementIndex = 0 }); +} + +void Clay__CloneElementsWithExitTransition() { + Clay_Context* context = Clay_GetCurrentContext(); + int32_t nextIndex = context->layoutElements.capacity - 1; + int32_t nextChildIndex = context->layoutElementChildren.capacity - 1; + + for (int i = 0; i < context->transitionDatas.length; ++i) { + Clay__TransitionDataInternal *data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i); + Clay_TransitionElementConfig* config = &data->elementThisFrame->config.transition; + if (data->transitionOut) { + Clay__int32_tArray bfsBuffer = context->openLayoutElementStack; + bfsBuffer.length = 0; + Clay_LayoutElement* newElement = Clay_LayoutElementArray_Set_DontTouchLength(&context->layoutElements, nextIndex, *data->elementThisFrame); + Clay__StringArray_Set_DontTouchLength(&context->layoutElementIdStrings, nextIndex, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, data->elementThisFrame - context->layoutElements.internalArray)); + Clay__int32_tArray_Add(&bfsBuffer, nextIndex); + data->elementThisFrame = newElement; + nextIndex--; + + int32_t bufferIndex = 0; + while(bufferIndex < bfsBuffer.length) { + Clay_LayoutElement *layoutElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, Clay__int32_tArray_GetValue(&bfsBuffer, bufferIndex)); + bufferIndex++; + for (int j = layoutElement->children.length - 1; j >= 0; --j) { + Clay_LayoutElement* childElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, layoutElement->children.elements[j]); + Clay__int32_tArray_Add(&bfsBuffer, nextIndex); + Clay_LayoutElement* newChildElement = Clay_LayoutElementArray_Set_DontTouchLength(&context->layoutElements, nextIndex, *childElement); + Clay__StringArray_Set_DontTouchLength(&context->layoutElementIdStrings, nextIndex, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, childElement - context->layoutElements.internalArray)); + Clay__int32_tArray_Set_DontTouchLength(&context->layoutElementChildren, nextChildIndex, nextIndex); + nextIndex--; + nextChildIndex--; + } + layoutElement->children.elements = &context->layoutElementChildren.internalArray[nextChildIndex + 1]; + } + } + } +}; + +void Clay_ApplyTransitionedPropertiesToElement(Clay_LayoutElement* currentElement, Clay_TransitionProperty properties, Clay_TransitionData currentTransitionData, Clay_BoundingBox* boundingBox, bool reparented) { + if (properties & CLAY_TRANSITION_PROPERTY_WIDTH) { + if (!reparented) { + currentElement->dimensions.width = currentTransitionData.boundingBox.width; + currentElement->config.layout.sizing.width = CLAY_SIZING_FIXED(currentTransitionData.boundingBox.width); + } else { + boundingBox->width = currentTransitionData.boundingBox.width; + } + } + if (properties & CLAY_TRANSITION_PROPERTY_HEIGHT) { + if (!reparented) { + currentElement->dimensions.height = currentTransitionData.boundingBox.height; + currentElement->config.layout.sizing.height = CLAY_SIZING_FIXED(currentTransitionData.boundingBox.height); + } else { + boundingBox->height = currentTransitionData.boundingBox.height; + } + } + if (properties & CLAY_TRANSITION_PROPERTY_X) { + boundingBox->x = currentTransitionData.boundingBox.x; + } + if (properties & CLAY_TRANSITION_PROPERTY_Y) { + boundingBox->y = currentTransitionData.boundingBox.y; + } + if (properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) { + currentElement->config.overlayColor = currentTransitionData.overlayColor; + } + if (properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) { + currentElement->config.backgroundColor = currentTransitionData.backgroundColor; + } + if (properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) { + currentElement->config.border.color = currentTransitionData.borderColor; + } + if (properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) { + currentElement->config.border.width = currentTransitionData.borderWidth; + } +} + +CLAY_WASM_EXPORT("Clay_EndLayout") +Clay_RenderCommandArray Clay_EndLayout(float deltaTime) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay__CloseElement(); + + if (context->openLayoutElementStack.length > 1) { + context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { + .errorType = CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE, + .errorText = CLAY_STRING("There were still open layout elements when EndLayout was called. This results from an unequal number of calls to Clay__OpenElement and Clay__CloseElement."), + .userData = context->errorHandler.userData }); + } + + // Prune non exiting transitions + for (int i = 0; i < context->transitionDatas.length; ++i) { + Clay__TransitionDataInternal *data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i); + Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(data->elementId); + // Transition element exited and doesn't have an exit handler defined + // Or, the user deleted the transition handler from one frame to the next + if (!data->transitionOut && (hashMapItem->generation <= context->generation || !hashMapItem->layoutElement->config.transition.handler)) { + Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, i); + i--; + continue; + } + } + + Clay__int32_tArray elementIdsToRemoveTransitions = context->reusableElementIndexBuffer; + elementIdsToRemoveTransitions.length = 0; + + for (int i = 0; i < context->transitionDatas.length; ++i) { + Clay__TransitionDataInternal *data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i); + Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(data->elementId); + // This might seems strange - can't we just look up the element itself, and check the config to see whether it has an exit transition defined? + // That would work fine if the element actually had an exit transition in the first place. If it doesn't have an exit transition defined, the element + // will have simply disappeared completely at this point, and there will be no element through which to access the config. + if (data->transitionOut) { + Clay_TransitionElementConfig* config = &data->elementThisFrame->config.transition; + // Element wasn't found this frame - either delete transition data or transition out + if (hashMapItem->generation <= context->generation) { + Clay_LayoutElementHashMapItem *parentHashMapItem = Clay__GetHashMapItem(data->parentId); + // Don't exit transition if the parent has also exited and SKIP_WHEN_PARENT_EXITS is used + if (config->exit.trigger == CLAY_TRANSITION_EXIT_TRIGGER_WHEN_PARENT_EXITS || !parentHashMapItem || parentHashMapItem->generation > context->generation) { + // This if only runs one single time when the element first starts exiting + if (data->state != CLAY_TRANSITION_STATE_EXITING) { + if (parentHashMapItem->generation <= context->generation) { + data->elementThisFrame->config.floating.attachTo = CLAY_ATTACH_TO_ROOT; + data->elementThisFrame->config.floating.offset = CLAY__INIT(Clay_Vector2) { hashMapItem->boundingBox.x, hashMapItem->boundingBox.y }; + data->elementThisFrame->config.floating.parentId = Clay__HashString(CLAY_STRING("Clay__RootContainer"), 0).id; + } + hashMapItem->appearedThisFrame = false; + data->elementThisFrame->exiting = true; + data->elementThisFrame->config.layout.sizing.width = CLAY_SIZING_FIXED(data->elementThisFrame->dimensions.width); + data->elementThisFrame->config.layout.sizing.height = CLAY_SIZING_FIXED(data->elementThisFrame->dimensions.height); + data->state = CLAY_TRANSITION_STATE_EXITING; + data->activeProperties = config->properties; + data->elapsedTime = 0; + data->targetState = config->exit.setFinalState(data->targetState, config->properties); + } + + // Below this line runs every frame while element is exiting ----------- + + // Clone the entire subtree back into the main UI layout tree + Clay__int32_tArray bfsBuffer = context->openLayoutElementStack; + bfsBuffer.length = 0; + data->elementThisFrame = Clay_LayoutElementArray_Add(&context->layoutElements, *data->elementThisFrame); + int32_t exitingElementIndex = data->elementThisFrame - context->layoutElements.internalArray; + Clay__StringArray_Add(&context->layoutElementIdStrings, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, exitingElementIndex)); + Clay__int32_tArray_Add(&context->layoutElementClipElementIds, *Clay__int32_tArray_GetCheckCapacity(&context->layoutElementClipElementIds, exitingElementIndex)); + Clay__int32_tArray_Add(&bfsBuffer, exitingElementIndex); + int32_t bufferIndex = 0; + while (bufferIndex < bfsBuffer.length) { + Clay_LayoutElement *layoutElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, Clay__int32_tArray_GetValue(&bfsBuffer, bufferIndex)); + Clay_LayoutElementHashMapItem* bfsMapItem = Clay__GetHashMapItem(layoutElement->id); + // Children of exiting elements may have been moved elsewhere in the layout, this prevents a duplicate ID error if they still exist. + if (bfsMapItem->generation <= context->generation) { + Clay__AddHashMapItem(CLAY__INIT(Clay_ElementId){ layoutElement->id }, layoutElement); + int32_t firstChildSlot = context->layoutElementChildren.length; + uint16_t newChildrenLength = layoutElement->children.length; + for (int j = 0; j < layoutElement->children.length; ++j) { + Clay_LayoutElement* childElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, layoutElement->children.elements[j]); + Clay_LayoutElementHashMapItem* childMapItem = Clay__GetHashMapItem(childElement->id); + if (childMapItem->generation <= context->generation) { + // Remove any nested transitions inside exiting trees + if (!childElement->isTextElement && childElement->config.transition.handler) { + Clay__int32_tArray_Add(&elementIdsToRemoveTransitions, childElement->id); + } + int32_t childElementIndex = childElement - context->layoutElements.internalArray; + Clay_LayoutElement* newChildElement = Clay_LayoutElementArray_Add(&context->layoutElements, *childElement); + Clay__StringArray_Add(&context->layoutElementIdStrings, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, childElementIndex)); + Clay__int32_tArray_Add(&context->layoutElementClipElementIds, *Clay__int32_tArray_GetCheckCapacity(&context->layoutElementClipElementIds, childElementIndex)); + Clay__int32_tArray_Add(&bfsBuffer, context->layoutElements.length - 1); + if (newChildElement->isTextElement) { + newChildElement->textElementData.wrappedLines.length = 0; + } + Clay__int32_tArray_Add(&context->layoutElementChildren, context->layoutElements.length - 1); + } else { + newChildrenLength--; + } + } + layoutElement->children = CLAY__INIT(Clay__LayoutElementChildren) { + .elements = &context->layoutElementChildren.internalArray[firstChildSlot], + .length = newChildrenLength, + }; + } + bufferIndex++; + } + hashMapItem->layoutElement = data->elementThisFrame; + + // Reattach the inserted subtree to its previous parent if it still exists + // and the exiting element is not floating + Clay_FloatingElementConfig* floatingConfig = &hashMapItem->layoutElement->config.floating; + if (parentHashMapItem->generation > context->generation && floatingConfig->attachTo == CLAY_ATTACH_TO_NONE) { + Clay_LayoutElement *parentElement = parentHashMapItem->layoutElement; + int32_t newChildrenStartIndex = context->layoutElementChildren.length; + bool found = false; + if (config->exit.siblingOrdering == CLAY_EXIT_TRANSITION_ORDERING_UNDERNEATH_SIBLINGS) { + Clay__int32_tArray_Add(&context->layoutElementChildren, exitingElementIndex); + found = true; + } + for (int j = 0; j < parentElement->children.length; ++j) { + if (config->exit.siblingOrdering == CLAY_EXIT_TRANSITION_ORDERING_NATURAL_ORDER && j == data->siblingIndex) { + Clay__int32_tArray_Add(&context->layoutElementChildren, exitingElementIndex); + found = true; + } + Clay__int32_tArray_Add(&context->layoutElementChildren, parentElement->children.elements[j]); + } + if (!found) { + Clay__int32_tArray_Add(&context->layoutElementChildren, exitingElementIndex); + } + parentElement->children.length++; + parentElement->children.elements = &context->layoutElementChildren.internalArray[newChildrenStartIndex]; + // Otherwise, create the tree root for the floating element (needs to be created every frame) + } else { + Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) { + .layoutElementIndex = (int32_t)(data->elementThisFrame - context->layoutElements.internalArray), + .parentId = floatingConfig->parentId, + .zIndex = floatingConfig->zIndex, + }); + } + // Parent exited, just delete child without exit transition + } else { + Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, i); + i--; + continue; + } + } + } + } + + for (int i = 0; i < elementIdsToRemoveTransitions.length; ++i) { + for (int j = 0; j < context->transitionDatas.length; ++j) { + if (Clay__TransitionDataInternalArray_Get(&context->transitionDatas, j)->elementId == Clay__int32_tArray_GetValue(&elementIdsToRemoveTransitions, i)) { + Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, j); + break; + } + } + } + + if (context->booleanWarnings.maxElementsExceeded) { + Clay_String message; + message = CLAY_STRING("Clay Error: Layout elements exceeded Clay__maxElementCount"); + Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand ) { + .boundingBox = { context->layoutDimensions.width / 2 - 59 * 4, context->layoutDimensions.height / 2, 0, 0 }, + .renderData = { .text = { .stringContents = CLAY__INIT(Clay_StringSlice) { .length = message.length, .chars = message.chars, .baseChars = message.chars }, .textColor = {255, 0, 0, 255}, .fontSize = 16 } }, + .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT + }); + } else { + if (context->transitionDatas.length > 0) { + Clay__CalculateFinalLayout(deltaTime, false, false); + + for (int i = 0; i < context->transitionDatas.length; ++i) { + Clay__TransitionDataInternal* transitionData = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i); + Clay_LayoutElement* currentElement = transitionData->elementThisFrame; + Clay_LayoutElementHashMapItem* mapItem = Clay__GetHashMapItem(transitionData->elementId); + Clay_LayoutElementHashMapItem* parentMapItem = Clay__GetHashMapItem(transitionData->parentId); + Clay_TransitionData targetState = transitionData->targetState; + if (transitionData->state != CLAY_TRANSITION_STATE_EXITING) { + targetState = CLAY__INIT(Clay_TransitionData) { + mapItem->boundingBox, + currentElement->config.backgroundColor, + currentElement->config.overlayColor, + currentElement->config.border.color, + currentElement->config.border.width, + }; + } + Clay_TransitionData oldTargetState = transitionData->targetState; + transitionData->targetState = targetState; + if (mapItem->appearedThisFrame) { + if (currentElement->config.transition.enter.setInitialState && !(parentMapItem->appearedThisFrame && currentElement->config.transition.enter.trigger == CLAY_TRANSITION_ENTER_SKIP_ON_FIRST_PARENT_FRAME)) { + transitionData->state = CLAY_TRANSITION_STATE_ENTERING; + transitionData->initialState = currentElement->config.transition.enter.setInitialState(transitionData->targetState, currentElement->config.transition.properties); + transitionData->currentState = transitionData->initialState; + transitionData->activeProperties = currentElement->config.transition.properties; + Clay_ApplyTransitionedPropertiesToElement(currentElement, currentElement->config.transition.properties, transitionData->initialState, &mapItem->boundingBox, transitionData->reparented); + } else { + transitionData->initialState = targetState; + transitionData->currentState = targetState; + transitionData->activeProperties = CLAY_TRANSITION_PROPERTY_NONE; + } + } else { + if (transitionData->state != CLAY_TRANSITION_STATE_EXITING) { + Clay_Vector2 parentScrollOffset = parentMapItem->layoutElement->config.clip.childOffset; + Clay_Vector2 newRelativePosition = { + mapItem->boundingBox.x - parentMapItem->boundingBox.x - parentScrollOffset.x, + mapItem->boundingBox.y - parentMapItem->boundingBox.y - parentScrollOffset.y, + }; + Clay_Vector2 oldRelativePosition = transitionData->oldParentRelativePosition; + transitionData->oldParentRelativePosition = newRelativePosition; + Clay_TransitionProperty properties = currentElement->config.transition.properties; + int32_t newActiveProperties = CLAY_TRANSITION_PROPERTY_NONE; + if (properties & CLAY_TRANSITION_PROPERTY_X) { + // Don't trigger a transition if... + if ( + // The element's absolute position didn't change + !Clay__FloatEqual(oldTargetState.boundingBox.x, targetState.boundingBox.x) + // The element is still in the same parent container, and it's parent-relative position didn't change (parent moved) + && (!(Clay__FloatEqual(oldRelativePosition.x, newRelativePosition.x)) || transitionData->reparented) + // The position changed was triggered by the outer window resizing + && !context->rootResizedLastFrame + ) { + newActiveProperties |= CLAY_TRANSITION_PROPERTY_X; + } + } + if (properties & CLAY_TRANSITION_PROPERTY_Y) { + // See extended comments above in PROPERTY_X for explanation + if (!Clay__FloatEqual(oldTargetState.boundingBox.y, targetState.boundingBox.y) && (!(Clay__FloatEqual(oldRelativePosition.y, newRelativePosition.y)) || transitionData->reparented) && !context->rootResizedLastFrame) { + newActiveProperties |= CLAY_TRANSITION_PROPERTY_Y; + } + } + if (properties & CLAY_TRANSITION_PROPERTY_WIDTH) { + if (!Clay__FloatEqual(oldTargetState.boundingBox.width, targetState.boundingBox.width) && !context->rootResizedLastFrame) { + newActiveProperties |= CLAY_TRANSITION_PROPERTY_WIDTH; + } + } + if (properties & CLAY_TRANSITION_PROPERTY_HEIGHT) { + if (!Clay__FloatEqual(oldTargetState.boundingBox.height, targetState.boundingBox.height) && !context->rootResizedLastFrame) { + newActiveProperties |= CLAY_TRANSITION_PROPERTY_HEIGHT; + } + } + if (properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) { + if (!Clay__MemCmp((char *) &oldTargetState.backgroundColor, (char *)&targetState.backgroundColor, sizeof(Clay_Color))) { + newActiveProperties |= CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR; + } + } + if (properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) { + if (!Clay__MemCmp((char *) &oldTargetState.overlayColor, (char *)&targetState.overlayColor, sizeof(Clay_Color))) { + newActiveProperties |= CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR; + } + } + if (properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) { + if (!Clay__MemCmp((char *) &oldTargetState.borderColor, (char *)&targetState.borderColor, sizeof(Clay_Color))) { + newActiveProperties |= CLAY_TRANSITION_PROPERTY_BORDER_COLOR; + } + } + if (properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) { + if (!Clay__MemCmp((char *) &oldTargetState.borderWidth, (char *)&targetState.borderWidth, sizeof(Clay_BorderWidth))) { + newActiveProperties |= CLAY_TRANSITION_PROPERTY_BORDER_WIDTH; + } + } + + if (newActiveProperties != 0) { + transitionData->elapsedTime = 0; + transitionData->initialState = transitionData->currentState; + transitionData->state = CLAY_TRANSITION_STATE_TRANSITIONING; + transitionData->activeProperties = (Clay_TransitionProperty)(transitionData->activeProperties | newActiveProperties); + } + } + + if (transitionData->state == CLAY_TRANSITION_STATE_IDLE) { + transitionData->initialState = targetState; + transitionData->currentState = targetState; + transitionData->targetState = targetState; + transitionData->activeProperties = CLAY_TRANSITION_PROPERTY_NONE; + } else { + bool transitionComplete = true; + transitionComplete = currentElement->config.transition.handler(CLAY__INIT(Clay_TransitionCallbackArguments) { + transitionData->state, + transitionData->initialState, + &transitionData->currentState, + targetState, + transitionData->elapsedTime, + currentElement->config.transition.duration, + transitionData->activeProperties + }); + + Clay_ApplyTransitionedPropertiesToElement(currentElement, transitionData->activeProperties, transitionData->currentState, &mapItem->boundingBox, transitionData->reparented); + transitionData->elapsedTime += deltaTime; + + if (transitionComplete) { + if (transitionData->state == CLAY_TRANSITION_STATE_ENTERING || transitionData->state == CLAY_TRANSITION_STATE_TRANSITIONING) {transitionData->state = CLAY_TRANSITION_STATE_IDLE; + transitionData->elapsedTime = 0; + transitionData->reparented = false; + transitionData->activeProperties = CLAY_TRANSITION_PROPERTY_NONE; + } else if (transitionData->state == CLAY_TRANSITION_STATE_EXITING) { + Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, i); + } + } + } + } + } + + if (context->debugModeEnabled) { + context->warningsEnabled = false; + Clay__RenderDebugView(); + context->warningsEnabled = true; + } + + if (context->booleanWarnings.maxElementsExceeded) { + Clay_String message; + message = CLAY_STRING("Clay Error: Debug view caused layout element count to exceed Clay__maxElementCount"); + Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand ) { + .boundingBox = { context->layoutDimensions.width / 2 - 59 * 4, context->layoutDimensions.height / 2, 0, 0 }, + .renderData = { .text = { .stringContents = CLAY__INIT(Clay_StringSlice) { .length = message.length, .chars = message.chars, .baseChars = message.chars }, .textColor = {255, 0, 0, 255}, .fontSize = 16 } }, + .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT + }); + } else { + Clay__CalculateFinalLayout(deltaTime, true, true); + Clay__CloneElementsWithExitTransition(); + } + } else { + if (context->debugModeEnabled) { + context->warningsEnabled = false; + Clay__RenderDebugView(); + context->warningsEnabled = true; + } + + if (context->booleanWarnings.maxElementsExceeded) { + Clay_String message; + message = CLAY_STRING("Clay Error: Debug view caused layout element count to exceed Clay__maxElementCount"); + Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand ) { + .boundingBox = { context->layoutDimensions.width / 2 - 59 * 4, context->layoutDimensions.height / 2, 0, 0 }, + .renderData = { .text = { .stringContents = CLAY__INIT(Clay_StringSlice) { .length = message.length, .chars = message.chars, .baseChars = message.chars }, .textColor = {255, 0, 0, 255}, .fontSize = 16 } }, + .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT + }); + } else { + Clay__CalculateFinalLayout(deltaTime, false, true); + } + } + } + + for (int i = 0; i < context->layoutElementsHashMap.capacity; ++i) { + int32_t currentElementIndex = context->layoutElementsHashMap.internalArray[i]; + int32_t previousElementIndex = -1; + while (currentElementIndex != -1) { + Clay_LayoutElementHashMapItem* currentItem = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, currentElementIndex); + int32_t nextIndex = currentItem->nextIndex; + // Needs to be pruned + if (currentItem->generation <= context->generation) { + // Delete the underlying item and add it to the freelist + Clay__LayoutElementHashMapItemArray_Set(&context->layoutElementsHashMapInternal, currentElementIndex, CLAY__INIT(Clay_LayoutElementHashMapItem) { .nextIndex = -1 }); + Clay__int32_tArray_Add(&context->layoutElementsHashMapFreeList, currentElementIndex); + // If it's the very top of the bucket, rewrite the first bucket pointer + if (previousElementIndex == -1) { + Clay__int32_tArray_Set(&context->layoutElementsHashMap, i, nextIndex); + currentElementIndex = nextIndex; + previousElementIndex = -1; + } else { + // Rewrite previous pointer + Clay_LayoutElementHashMapItem* previousItem = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, previousElementIndex); + previousItem->nextIndex = nextIndex; + currentElementIndex = nextIndex; + } + } else { + previousElementIndex = currentElementIndex; + currentElementIndex = nextIndex; + } + } + } + + return context->renderCommands; +} + +CLAY_WASM_EXPORT("Clay_GetOpenElementId") +uint32_t Clay_GetOpenElementId(void) { + return Clay__GetOpenLayoutElement()->id; +} + +CLAY_WASM_EXPORT("Clay_GetElementId") +Clay_ElementId Clay_GetElementId(Clay_String idString) { + return Clay__HashString(idString, 0); +} + +CLAY_WASM_EXPORT("Clay_GetElementIdWithIndex") +Clay_ElementId Clay_GetElementIdWithIndex(Clay_String idString, uint32_t index) { + return Clay__HashStringWithOffset(idString, index, 0); +} + +bool Clay_Hovered(void) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->booleanWarnings.maxElementsExceeded) { + return false; + } + Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement(); + for (int32_t i = 0; i < context->pointerOverIds.length; ++i) { + if (Clay_ElementIdArray_Get(&context->pointerOverIds, i)->id == openLayoutElement->id) { + return true; + } + } + return false; +} + +void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData), void *userData) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context->booleanWarnings.maxElementsExceeded) { + return; + } + Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement(); + Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(openLayoutElement->id); + hashMapItem->onHoverFunction = onHoverFunction; + hashMapItem->hoverFunctionUserData = userData; +} + +CLAY_WASM_EXPORT("Clay_PointerOver") +bool Clay_PointerOver(Clay_ElementId elementId) { // TODO return priority for separating multiple results + Clay_Context* context = Clay_GetCurrentContext(); + for (int32_t i = 0; i < context->pointerOverIds.length; ++i) { + if (Clay_ElementIdArray_Get(&context->pointerOverIds, i)->id == elementId.id) { + return true; + } + } + return false; +} + +CLAY_WASM_EXPORT("Clay_GetScrollContainerData") +Clay_ScrollContainerData Clay_GetScrollContainerData(Clay_ElementId id) { + Clay_Context* context = Clay_GetCurrentContext(); + for (int32_t i = 0; i < context->scrollContainerDatas.length; ++i) { + Clay__ScrollContainerDataInternal *scrollContainerData = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i); + if (scrollContainerData->elementId == id.id) { + if (!scrollContainerData->layoutElement) { // This can happen on the first frame before a scroll container is declared + return CLAY__INIT(Clay_ScrollContainerData) CLAY__DEFAULT_STRUCT; + } + return CLAY__INIT(Clay_ScrollContainerData) { + .scrollPosition = &scrollContainerData->scrollPosition, + .scrollContainerDimensions = { scrollContainerData->boundingBox.width, scrollContainerData->boundingBox.height }, + .contentDimensions = scrollContainerData->contentSize, + .config = scrollContainerData->layoutElement->config.clip, + .found = true + }; + } + } + return CLAY__INIT(Clay_ScrollContainerData) CLAY__DEFAULT_STRUCT; +} + +CLAY_WASM_EXPORT("Clay_GetElementData") +Clay_ElementData Clay_GetElementData(Clay_ElementId id){ + Clay_LayoutElementHashMapItem * item = Clay__GetHashMapItem(id.id); + if(item == &Clay_LayoutElementHashMapItem_DEFAULT) { + return CLAY__INIT(Clay_ElementData) CLAY__DEFAULT_STRUCT; + } + + return CLAY__INIT(Clay_ElementData){ + .boundingBox = item->boundingBox, + .found = true + }; +} + +CLAY_WASM_EXPORT("Clay_SetDebugModeEnabled") +void Clay_SetDebugModeEnabled(bool enabled) { + Clay_Context* context = Clay_GetCurrentContext(); + context->debugModeEnabled = enabled; +} + +CLAY_WASM_EXPORT("Clay_IsDebugModeEnabled") +bool Clay_IsDebugModeEnabled(void) { + Clay_Context* context = Clay_GetCurrentContext(); + return context->debugModeEnabled; +} + +CLAY_WASM_EXPORT("Clay_SetCullingEnabled") +void Clay_SetCullingEnabled(bool enabled) { + Clay_Context* context = Clay_GetCurrentContext(); + context->disableCulling = !enabled; +} + +CLAY_WASM_EXPORT("Clay_SetExternalScrollHandlingEnabled") +void Clay_SetExternalScrollHandlingEnabled(bool enabled) { + Clay_Context* context = Clay_GetCurrentContext(); + context->externalScrollHandlingEnabled = enabled; +} + +CLAY_WASM_EXPORT("Clay_GetMaxElementCount") +int32_t Clay_GetMaxElementCount(void) { + Clay_Context* context = Clay_GetCurrentContext(); + return context->maxElementCount; +} + +CLAY_WASM_EXPORT("Clay_SetMaxElementCount") +void Clay_SetMaxElementCount(int32_t maxElementCount) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context) { + context->maxElementCount = maxElementCount; + } else { + Clay__defaultMaxElementCount = maxElementCount; // TODO: Fix this + Clay__defaultMaxMeasureTextWordCacheCount = maxElementCount * 2; + } +} + +CLAY_WASM_EXPORT("Clay_GetMaxMeasureTextCacheWordCount") +int32_t Clay_GetMaxMeasureTextCacheWordCount(void) { + Clay_Context* context = Clay_GetCurrentContext(); + return context->maxMeasureTextCacheWordCount; +} + +CLAY_WASM_EXPORT("Clay_SetMaxMeasureTextCacheWordCount") +void Clay_SetMaxMeasureTextCacheWordCount(int32_t maxMeasureTextCacheWordCount) { + Clay_Context* context = Clay_GetCurrentContext(); + if (context) { + Clay__currentContext->maxMeasureTextCacheWordCount = maxMeasureTextCacheWordCount; + } else { + Clay__defaultMaxMeasureTextWordCacheCount = maxMeasureTextCacheWordCount; // TODO: Fix this + } +} + +CLAY_WASM_EXPORT("Clay_ResetMeasureTextCache") +void Clay_ResetMeasureTextCache(void) { + Clay_Context* context = Clay_GetCurrentContext(); + context->measureTextHashMapInternal.length = 0; + context->measureTextHashMapInternalFreeList.length = 0; + context->measureTextHashMap.length = 0; + context->measuredWords.length = 0; + context->measuredWordsFreeList.length = 0; + + for (int32_t i = 0; i < context->measureTextHashMap.capacity; ++i) { + context->measureTextHashMap.internalArray[i] = 0; + } + context->measureTextHashMapInternal.length = 1; // Reserve the 0 value to mean "no next element" +} + +#define CLAY__LERP(from, to, mix) (from + (to - from) * mix) + +CLAY_DLL_EXPORT bool Clay_EaseOut(Clay_TransitionCallbackArguments arguments) { + float ratio = 1; + if (arguments.duration > 0) { + ratio = CLAY__MIN(arguments.elapsedTime / arguments.duration, 1); + } + float inverse = 1.0f - ratio; + float lerpAmount = 1.0f - (inverse * inverse * inverse); + if (arguments.properties & CLAY_TRANSITION_PROPERTY_X) { + arguments.current->boundingBox.x = CLAY__LERP(arguments.initial.boundingBox.x, arguments.target.boundingBox.x, lerpAmount); + } + if (arguments.properties & CLAY_TRANSITION_PROPERTY_Y) { + arguments.current->boundingBox.y = CLAY__LERP(arguments.initial.boundingBox.y, arguments.target.boundingBox.y, lerpAmount); + } + if (arguments.properties & CLAY_TRANSITION_PROPERTY_WIDTH) { + arguments.current->boundingBox.width = CLAY__LERP(arguments.initial.boundingBox.width, arguments.target.boundingBox.width, lerpAmount); + } + if (arguments.properties & CLAY_TRANSITION_PROPERTY_HEIGHT) { + arguments.current->boundingBox.height = CLAY__LERP(arguments.initial.boundingBox.height, arguments.target.boundingBox.height, lerpAmount); + } + if (arguments.properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) { + arguments.current->backgroundColor = CLAY__INIT(Clay_Color) { + .r = CLAY__LERP(arguments.initial.backgroundColor.r, arguments.target.backgroundColor.r, lerpAmount), + .g = CLAY__LERP(arguments.initial.backgroundColor.g, arguments.target.backgroundColor.g, lerpAmount), + .b = CLAY__LERP(arguments.initial.backgroundColor.b, arguments.target.backgroundColor.b, lerpAmount), + .a = CLAY__LERP(arguments.initial.backgroundColor.a, arguments.target.backgroundColor.a, lerpAmount), + }; + } + if (arguments.properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) { + arguments.current->overlayColor = CLAY__INIT(Clay_Color) { + .r = CLAY__LERP(arguments.initial.overlayColor.r, arguments.target.overlayColor.r, lerpAmount), + .g = CLAY__LERP(arguments.initial.overlayColor.g, arguments.target.overlayColor.g, lerpAmount), + .b = CLAY__LERP(arguments.initial.overlayColor.b, arguments.target.overlayColor.b, lerpAmount), + .a = CLAY__LERP(arguments.initial.overlayColor.a, arguments.target.overlayColor.a, lerpAmount), + }; + } + if (arguments.properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) { + arguments.current->borderColor = CLAY__INIT(Clay_Color) { + .r = CLAY__LERP(arguments.initial.borderColor.r, arguments.target.borderColor.r, lerpAmount), + .g = CLAY__LERP(arguments.initial.borderColor.g, arguments.target.borderColor.g, lerpAmount), + .b = CLAY__LERP(arguments.initial.borderColor.b, arguments.target.borderColor.b, lerpAmount), + .a = CLAY__LERP(arguments.initial.borderColor.a, arguments.target.borderColor.a, lerpAmount), + }; + } + if (arguments.properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) { + arguments.current->borderWidth = CLAY__INIT(Clay_BorderWidth) { + .left = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.left, arguments.target.borderWidth.left, lerpAmount), + .right = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.right, arguments.target.borderWidth.right, lerpAmount), + .top = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.top, arguments.target.borderWidth.top, lerpAmount), + .bottom = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.bottom, arguments.target.borderWidth.bottom, lerpAmount), + .betweenChildren = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.betweenChildren, arguments.target.borderWidth.betweenChildren, lerpAmount), + }; + } + return ratio >= 1; +} + +#endif // CLAY_IMPLEMENTATION + +/* +LICENSE +zlib/libpng license + +Copyright (c) 2024 Nic Barker + +This software is provided 'as-is', without any express or implied warranty. +In no event will the authors be held liable for any damages arising from the +use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ diff --git a/Dependencies/clay/premake5.lua b/Dependencies/clay/premake5.lua new file mode 100644 index 000000000..dfca41d46 --- /dev/null +++ b/Dependencies/clay/premake5.lua @@ -0,0 +1,27 @@ +project "clay" + kind "StaticLib" + language "C" + cdialect "C17" + targetdir (outputdir .. "%{cfg.buildcfg}/%{prj.name}") + objdir (objoutdir .. "%{cfg.buildcfg}/%{prj.name}") + warnings "Off" + + files { + "include/**.h", + "src/**.c", + "**.lua" + } + + includedirs { + "include" + } + + filter { "configurations:Debug" } + defines { "DEBUG", "_DEBUG" } + runtime "Debug" + symbols "On" + + filter { "configurations:Release or configurations:Publish" } + defines { "NDEBUG" } + runtime "Release" + optimize "On" diff --git a/Dependencies/clay/src/clay.c b/Dependencies/clay/src/clay.c new file mode 100644 index 000000000..105cf4c56 --- /dev/null +++ b/Dependencies/clay/src/clay.c @@ -0,0 +1,8 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#define CLAY_IMPLEMENTATION +#include diff --git a/Resources/Engine/Lua/Components/Canvas.lua b/Resources/Engine/Lua/Components/Canvas.lua new file mode 100644 index 000000000..3d64002fc --- /dev/null +++ b/Resources/Engine/Lua/Components/Canvas.lua @@ -0,0 +1,71 @@ +---@meta + +--- Defines how a Canvas scales UI elements +---@enum CanvasScalerMode +CanvasScalerMode = { + CONSTANT_PIXEL_SIZE = 0, + SCALE_WITH_SCREEN_SIZE = 1 +} + +---@enum CanvasScreenMatchMode +CanvasScreenMatchMode = { + MATCH_WIDTH_OR_HEIGHT = 0, + EXPAND = 1, + SHRINK = 2 +} + +--- Represents a root canvas for in-game user interface elements +---@class Canvas : Component +Canvas = {} + +--- Returns the actor that owns this component +---@return Actor +function Canvas:GetOwner() end + +--- Returns the reference resolution used by the canvas +---@return Vector2 +function Canvas:GetReferenceResolution() end + +--- Defines the reference resolution used by the canvas +---@param referenceResolution Vector2 +function Canvas:SetReferenceResolution(referenceResolution) end + +--- Returns the canvas scale factor +---@return number +function Canvas:GetScaleFactor() end + +--- Defines the canvas scale factor +---@param scaleFactor number +function Canvas:SetScaleFactor(scaleFactor) end + +--- Returns the number of UI pixels represented by one world unit +---@return number +function Canvas:GetPixelsPerUnit() end + +--- Defines the number of UI pixels represented by one world unit +---@param pixelsPerUnit number +function Canvas:SetPixelsPerUnit(pixelsPerUnit) end + +--- Returns the canvas scaler mode +---@return CanvasScalerMode +function Canvas:GetScalerMode() end + +--- Defines the canvas scaler mode +---@param scalerMode CanvasScalerMode +function Canvas:SetScalerMode(scalerMode) end + +--- Returns the screen match mode used with SCALE_WITH_SCREEN_SIZE +---@return CanvasScreenMatchMode +function Canvas:GetScreenMatchMode() end + +--- Defines the screen match mode used with SCALE_WITH_SCREEN_SIZE +---@param screenMatchMode CanvasScreenMatchMode +function Canvas:SetScreenMatchMode(screenMatchMode) end + +--- Returns the match width/height factor in range [0, 1] +---@return number +function Canvas:GetMatchWidthOrHeight() end + +--- Defines the match width/height factor in range [0, 1] +---@param value number +function Canvas:SetMatchWidthOrHeight(value) end diff --git a/Resources/Engine/Lua/Components/HorizontalLayout.lua b/Resources/Engine/Lua/Components/HorizontalLayout.lua new file mode 100644 index 000000000..bed6f164a --- /dev/null +++ b/Resources/Engine/Lua/Components/HorizontalLayout.lua @@ -0,0 +1,9 @@ +---@meta + +--- Arranges direct user interface children horizontally +---@class HorizontalLayout : LayoutGroup +HorizontalLayout = {} + +--- Returns the actor that owns this component +---@return Actor +function HorizontalLayout:GetOwner() end diff --git a/Resources/Engine/Lua/Components/Image.lua b/Resources/Engine/Lua/Components/Image.lua new file mode 100644 index 000000000..5459c3870 --- /dev/null +++ b/Resources/Engine/Lua/Components/Image.lua @@ -0,0 +1,33 @@ +---@meta + +--- Represents a renderable user interface image +---@class Image : Component +Image = {} + +--- Returns the actor that owns this component +---@return Actor +function Image:GetOwner() end + +--- Returns the texture rendered by the image +---@return Texture|nil +function Image:GetTexture() end + +--- Defines the texture rendered by the image +---@param texture Texture|nil +function Image:SetTexture(texture) end + +--- Returns the image size +---@return Vector2 +function Image:GetSize() end + +--- Defines the image size +---@param size Vector2 +function Image:SetSize(size) end + +--- Returns the image tint +---@return Vector4 +function Image:GetTint() end + +--- Defines the image tint +---@param tint Vector4 +function Image:SetTint(tint) end diff --git a/Resources/Engine/Lua/Components/LayoutGroup.lua b/Resources/Engine/Lua/Components/LayoutGroup.lua new file mode 100644 index 000000000..4306ba569 --- /dev/null +++ b/Resources/Engine/Lua/Components/LayoutGroup.lua @@ -0,0 +1,94 @@ +---@meta + +--- Defines how a LayoutGroup arranges direct UI children +---@enum LayoutDirection +LayoutDirection = { + HORIZONTAL = 0, + VERTICAL = 1 +} + +---@enum LayoutHorizontalAlignment +LayoutHorizontalAlignment = { + LEFT = 0, + CENTER = 1, + RIGHT = 2 +} + +---@enum LayoutVerticalAlignment +LayoutVerticalAlignment = { + TOP = 0, + CENTER = 1, + BOTTOM = 2 +} + +--- Arranges direct user interface children along an axis +---@class LayoutGroup : Component +LayoutGroup = {} + +--- Returns the actor that owns this component +---@return Actor +function LayoutGroup:GetOwner() end + +--- Returns the layout direction +---@return LayoutDirection +function LayoutGroup:GetDirection() end + +--- Defines the layout direction +---@param direction LayoutDirection +function LayoutGroup:SetDirection(direction) end + +--- Returns the spacing between children +---@return number +function LayoutGroup:GetSpacing() end + +--- Defines the non-negative spacing between children +---@param spacing number +function LayoutGroup:SetSpacing(spacing) end + +--- Returns the minimum layout container size +---@return Vector2 +function LayoutGroup:GetSize() end + +--- Defines the minimum layout container size +---@param size Vector2 +function LayoutGroup:SetSize(size) end + +--- Returns the layout padding as left, right, top, bottom +---@return Vector4 +function LayoutGroup:GetPadding() end + +--- Defines the layout padding as left, right, top, bottom +---@param padding Vector4 +function LayoutGroup:SetPadding(padding) end + +--- Returns the horizontal children alignment +---@return LayoutHorizontalAlignment +function LayoutGroup:GetHorizontalAlignment() end + +--- Defines the horizontal children alignment +---@param alignment LayoutHorizontalAlignment +function LayoutGroup:SetHorizontalAlignment(alignment) end + +--- Returns the vertical children alignment +---@return LayoutVerticalAlignment +function LayoutGroup:GetVerticalAlignment() end + +--- Defines the vertical children alignment +---@param alignment LayoutVerticalAlignment +function LayoutGroup:SetVerticalAlignment(alignment) end + +--- Returns whether the layout controls children width +---@return boolean +function LayoutGroup:GetControlChildrenWidth() end + +--- Defines whether the layout controls children width +---@param value boolean +function LayoutGroup:SetControlChildrenWidth(value) end + +--- Returns whether the layout controls children height +---@return boolean +function LayoutGroup:GetControlChildrenHeight() end + +--- Defines whether the layout controls children height +---@param value boolean +function LayoutGroup:SetControlChildrenHeight(value) end diff --git a/Resources/Engine/Lua/Components/Text.lua b/Resources/Engine/Lua/Components/Text.lua new file mode 100644 index 000000000..b295a9be6 --- /dev/null +++ b/Resources/Engine/Lua/Components/Text.lua @@ -0,0 +1,79 @@ +---@meta + +---@enum TextHorizontalAlignment +TextHorizontalAlignment = { + LEFT = 0, + CENTER = 1, + RIGHT = 2 +} + +---@enum TextVerticalAlignment +TextVerticalAlignment = { + TOP = 0, + CENTER = 1, + BOTTOM = 2 +} + +--- Represents a renderable user interface text +---@class Text : Component +Text = {} + +--- Returns the actor that owns this component +---@return Actor +function Text:GetOwner() end + +--- Returns the text content +---@return string +function Text:GetText() end + +--- Defines the text content +---@param text string +function Text:SetText(text) end + +--- Returns the font resource path +---@return string +function Text:GetFontPath() end + +--- Defines the font resource path +---@param fontPath string +function Text:SetFontPath(fontPath) end + +--- Returns the font size in canvas pixels +---@return number +function Text:GetFontSize() end + +--- Defines the font size in canvas pixels +---@param fontSize number +function Text:SetFontSize(fontSize) end + +--- Returns the text color +---@return Vector4 +function Text:GetColor() end + +--- Defines the text color +---@param color Vector4 +function Text:SetColor(color) end + +--- Returns the text extents in canvas pixels +---@return Vector2 +function Text:GetExtents() end + +--- Defines the text extents in canvas pixels +---@param extents Vector2 +function Text:SetExtents(extents) end + +--- Returns the horizontal text alignment +---@return TextHorizontalAlignment +function Text:GetHorizontalAlignment() end + +--- Defines the horizontal text alignment +---@param alignment TextHorizontalAlignment +function Text:SetHorizontalAlignment(alignment) end + +--- Returns the vertical text alignment +---@return TextVerticalAlignment +function Text:GetVerticalAlignment() end + +--- Defines the vertical text alignment +---@param alignment TextVerticalAlignment +function Text:SetVerticalAlignment(alignment) end diff --git a/Resources/Engine/Lua/Components/Transform2D.lua b/Resources/Engine/Lua/Components/Transform2D.lua new file mode 100644 index 000000000..50b8c4e82 --- /dev/null +++ b/Resources/Engine/Lua/Components/Transform2D.lua @@ -0,0 +1,78 @@ +---@meta + +--- Represents 2D transformations applied to a user interface actor +---@class Transform2D : Component +Transform2D = {} + +--- Defines the anchor preset used by Transform2D +---@enum AnchorPreset +AnchorPreset = { + TOP_LEFT = 0, + TOP_CENTER = 1, + TOP_RIGHT = 2, + MIDDLE_LEFT = 3, + CENTER = 4, + MIDDLE_RIGHT = 5, + BOTTOM_LEFT = 6, + BOTTOM_CENTER = 7, + BOTTOM_RIGHT = 8, + HORIZONTAL_STRETCH_TOP = 9, + HORIZONTAL_STRETCH_MIDDLE = 10, + HORIZONTAL_STRETCH_BOTTOM = 11, + VERTICAL_STRETCH_LEFT = 12, + VERTICAL_STRETCH_CENTER = 13, + VERTICAL_STRETCH_RIGHT = 14, + STRETCH_BOTH = 15 +} + +--- Returns the actor that owns this component +---@return Actor +function Transform2D:GetOwner() end + +--- Returns the 2D position +---@return Vector2 +function Transform2D:GetPosition() end + +--- Defines the 2D position +---@param position Vector2 +function Transform2D:SetPosition(position) end + +--- Returns the 2D rotation in degrees +---@return number +function Transform2D:GetRotation() end + +--- Defines the 2D rotation in degrees +---@param rotation number +function Transform2D:SetRotation(rotation) end + +--- Returns the 2D scale +---@return Vector2 +function Transform2D:GetScale() end + +--- Defines the 2D scale +---@param scale Vector2 +function Transform2D:SetScale(scale) end + +--- Returns the 2D size +---@return Vector2 +function Transform2D:GetSize() end + +--- Defines the 2D size +---@param size Vector2 +function Transform2D:SetSize(size) end + +--- Returns the normalized pivot in range [-1, 1] +---@return Vector2 +function Transform2D:GetPivot() end + +--- Defines the normalized pivot in range [-1, 1] +---@param pivot Vector2 +function Transform2D:SetPivot(pivot) end + +--- Returns the anchor preset +---@return AnchorPreset +function Transform2D:GetAnchorPreset() end + +--- Defines the anchor preset +---@param anchorPreset AnchorPreset +function Transform2D:SetAnchorPreset(anchorPreset) end diff --git a/Resources/Engine/Lua/Components/VerticalLayout.lua b/Resources/Engine/Lua/Components/VerticalLayout.lua new file mode 100644 index 000000000..a6199c17c --- /dev/null +++ b/Resources/Engine/Lua/Components/VerticalLayout.lua @@ -0,0 +1,9 @@ +---@meta + +--- Arranges direct user interface children vertically +---@class VerticalLayout : LayoutGroup +VerticalLayout = {} + +--- Returns the actor that owns this component +---@return Actor +function VerticalLayout:GetOwner() end diff --git a/Resources/Engine/Lua/Scene/Actor.lua b/Resources/Engine/Lua/Scene/Actor.lua index f1400bf21..d322aed1b 100644 --- a/Resources/Engine/Lua/Scene/Actor.lua +++ b/Resources/Engine/Lua/Scene/Actor.lua @@ -146,6 +146,34 @@ function Actor:GetPostProcessStack() end ---@return ReflectionProbe|nil function Actor:GetReflectionProbe() end +--- Returns the Canvas attached to this actor (If any) +---@return Canvas|nil +function Actor:GetCanvas() end + +--- Returns the Image attached to this actor (If any) +---@return Image|nil +function Actor:GetImage() end + +--- Returns the LayoutGroup attached to this actor (If any) +---@return LayoutGroup|nil +function Actor:GetLayoutGroup() end + +--- Returns the HorizontalLayout attached to this actor (If any) +---@return HorizontalLayout|nil +function Actor:GetHorizontalLayout() end + +--- Returns the VerticalLayout attached to this actor (If any) +---@return VerticalLayout|nil +function Actor:GetVerticalLayout() end + +--- Returns the Text attached to this actor (If any) +---@return Text|nil +function Actor:GetText() end + +--- Returns the Transform2D attached to this actor (If any) +---@return Transform2D|nil +function Actor:GetTransform2D() end + --- Returns the Behaviour of the given type attached to this actor (If any) ---@param name string ---@return table|nil @@ -219,6 +247,34 @@ function Actor:AddPostProcessStack() end ---@return ReflectionProbe function Actor:AddReflectionProbe() end +--- Adds a Canvas component to the actor and returns it +---@return Canvas +function Actor:AddCanvas() end + +--- Adds an Image component to the actor and returns it +---@return Image +function Actor:AddImage() end + +--- Adds a LayoutGroup component to the actor and returns it +---@return LayoutGroup +function Actor:AddLayoutGroup() end + +--- Adds a HorizontalLayout component to the actor and returns it +---@return HorizontalLayout +function Actor:AddHorizontalLayout() end + +--- Adds a VerticalLayout component to the actor and returns it +---@return VerticalLayout +function Actor:AddVerticalLayout() end + +--- Adds a Text component to the actor and returns it +---@return Text +function Actor:AddText() end + +--- Adds a Transform2D component to the actor and returns it +---@return Transform2D +function Actor:AddTransform2D() end + --- Removes the ModelRenderer component from the actor function Actor:RemoveModelRenderer() end --- Removes the PhysicalBox component from the actor @@ -251,6 +307,20 @@ function Actor:RemoveAudioListener() end function Actor:RemovePostProcessStack() end --- Removes the ReflectionProbe component from the actor function Actor:RemoveReflectionProbe() end +--- Removes the Canvas component from the actor +function Actor:RemoveCanvas() end +--- Removes the Image component from the actor +function Actor:RemoveImage() end +--- Removes the LayoutGroup component from the actor +function Actor:RemoveLayoutGroup() end +--- Removes the HorizontalLayout component from the actor +function Actor:RemoveHorizontalLayout() end +--- Removes the VerticalLayout component from the actor +function Actor:RemoveVerticalLayout() end +--- Removes the Text component from the actor +function Actor:RemoveText() end +--- Removes the Transform2D component from the actor +function Actor:RemoveTransform2D() end --- Adds a behaviour of given type to the actor and returns it ---@param name string diff --git a/Resources/Engine/Materials/UI_Image.ovmat b/Resources/Engine/Materials/UI_Image.ovmat new file mode 100644 index 000000000..da4c7a128 --- /dev/null +++ b/Resources/Engine/Materials/UI_Image.ovmat @@ -0,0 +1,36 @@ + + :Shaders\UI_Image.ovfx + + true + true + true + false + false + false + false + true + false + false + false + false + true + 1 + 1000 + + + + u_Image + ? + + + u_Tint + + 1 + 1 + 1 + 1 + + + + + diff --git a/Resources/Engine/Materials/UI_Text.ovmat b/Resources/Engine/Materials/UI_Text.ovmat new file mode 100644 index 000000000..228b6617a --- /dev/null +++ b/Resources/Engine/Materials/UI_Text.ovmat @@ -0,0 +1,36 @@ + + :Shaders\UI_Text.ovfx + + true + true + true + false + false + false + false + true + false + false + false + false + true + 1 + 1000 + + + + u_FontAtlas + ? + + + u_Color + + 1 + 1 + 1 + 1 + + + + + diff --git a/Resources/Engine/Shaders/UI_Image.ovfx b/Resources/Engine/Shaders/UI_Image.ovfx new file mode 100644 index 000000000..0bcaa4e3a --- /dev/null +++ b/Resources/Engine/Shaders/UI_Image.ovfx @@ -0,0 +1,30 @@ +#shader vertex +#version 450 core + +#include ":Shaders/Common/Buffers/EngineUBO.ovfxh" + +layout (location = 0) in vec3 geo_Pos; +layout (location = 1) in vec2 geo_TexCoords; + +out vec2 TexCoords; + +void main() +{ + TexCoords = geo_TexCoords; + gl_Position = ubo_Projection * ubo_View * ubo_Model * vec4(geo_Pos, 1.0); +} + +#shader fragment +#version 450 core + +in vec2 TexCoords; + +uniform sampler2D u_Image; +uniform vec4 u_Tint = vec4(1.0); + +out vec4 FRAGMENT_COLOR; + +void main() +{ + FRAGMENT_COLOR = texture(u_Image, TexCoords) * u_Tint; +} diff --git a/Resources/Engine/Shaders/UI_Text.ovfx b/Resources/Engine/Shaders/UI_Text.ovfx new file mode 100644 index 000000000..d7ac1febb --- /dev/null +++ b/Resources/Engine/Shaders/UI_Text.ovfx @@ -0,0 +1,31 @@ +#shader vertex +#version 450 core + +#include ":Shaders/Common/Buffers/EngineUBO.ovfxh" + +layout (location = 0) in vec3 geo_Pos; +layout (location = 1) in vec2 geo_TexCoords; + +out vec2 TexCoords; + +void main() +{ + TexCoords = geo_TexCoords; + gl_Position = ubo_Projection * ubo_View * ubo_Model * vec4(geo_Pos, 1.0); +} + +#shader fragment +#version 450 core + +in vec2 TexCoords; + +uniform sampler2D u_FontAtlas; +uniform vec4 u_Color = vec4(1.0); + +out vec4 FRAGMENT_COLOR; + +void main() +{ + const float alpha = texture(u_FontAtlas, TexCoords).a; + FRAGMENT_COLOR = vec4(u_Color.rgb, u_Color.a * alpha); +} diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/CCanvas.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/CCanvas.h new file mode 100644 index 000000000..ffe18aeda --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CCanvas.h @@ -0,0 +1,154 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI +{ + /** + * Represents a root canvas for in-game user interface elements + */ + class CCanvas : public AComponent + { + public: + enum class EScalerMode + { + CONSTANT_PIXEL_SIZE, + SCALE_WITH_SCREEN_SIZE + }; + + enum class EScreenMatchMode + { + MATCH_WIDTH_OR_HEIGHT, + EXPAND, + SHRINK + }; + + /** + * Constructor + * @param p_owner + */ + CCanvas(ECS::Actor& p_owner); + + /** + * Returns the name of the component + */ + std::string GetName() override; + + /** + * Returns the type name of the component + */ + virtual std::string GetTypeName() override; + + /** + * Sets the canvas reference resolution + * @param p_referenceResolution + */ + void SetReferenceResolution(const OvMaths::FVector2& p_referenceResolution); + + /** + * Returns the canvas reference resolution + */ + const OvMaths::FVector2& GetReferenceResolution() const; + + /** + * Sets the canvas scale factor + * @param p_scaleFactor + */ + void SetScaleFactor(float p_scaleFactor); + + /** + * Returns the canvas scale factor + */ + float GetScaleFactor() const; + + /** + * Sets the number of UI pixels represented by one world unit + * @param p_pixelsPerUnit + */ + void SetPixelsPerUnit(float p_pixelsPerUnit); + + /** + * Returns the number of UI pixels represented by one world unit + */ + float GetPixelsPerUnit() const; + + /** + * Sets the canvas scaler mode + * @param p_scalerMode + */ + void SetScalerMode(EScalerMode p_scalerMode); + + /** + * Returns the canvas scaler mode + */ + EScalerMode GetScalerMode() const; + + /** + * Sets the screen match mode used when scaler mode is Scale With Screen Size + * @param p_screenMatchMode + */ + void SetScreenMatchMode(EScreenMatchMode p_screenMatchMode); + + /** + * Returns the screen match mode used when scaler mode is Scale With Screen Size + */ + EScreenMatchMode GetScreenMatchMode() const; + + /** + * Sets the width/height match factor in range [0, 1] + * @param p_matchWidthOrHeight + */ + void SetMatchWidthOrHeight(float p_matchWidthOrHeight); + + /** + * Returns the width/height match factor in range [0, 1] + */ + float GetMatchWidthOrHeight() const; + + /** + * Serialize the component + * @param p_doc + * @param p_node + */ + virtual void OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Deserialize the component + * @param p_doc + * @param p_node + */ + virtual void OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Defines how the component should be drawn in the inspector + * @param p_root + */ + virtual void OnInspector(OvUI::Internal::WidgetContainer& p_root) override; + + private: + OvMaths::FVector2 m_referenceResolution = { 1920.0f, 1080.0f }; + float m_scaleFactor = 1.0f; + float m_pixelsPerUnit = 100.0f; + EScalerMode m_scalerMode = EScalerMode::SCALE_WITH_SCREEN_SIZE; + EScreenMatchMode m_screenMatchMode = EScreenMatchMode::MATCH_WIDTH_OR_HEIGHT; + float m_matchWidthOrHeight = 0.5f; + }; +} + +namespace OvCore::ECS::Components +{ + template<> + struct ComponentTraits + { + static constexpr std::string_view Name = "class OvCore::ECS::Components::UI::CCanvas"; + }; +} diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/CHorizontalLayout.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/CHorizontalLayout.h new file mode 100644 index 000000000..74217ddd1 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CHorizontalLayout.h @@ -0,0 +1,53 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +namespace OvCore::ECS::Components::UI +{ + /** + * Arranges direct user interface children horizontally + */ + class CHorizontalLayout : public CLayoutGroup + { + public: + /** + * Constructor + * @param p_owner + */ + CHorizontalLayout(ECS::Actor& p_owner); + + /** + * Returns the name of the component + */ + std::string GetName() override; + + /** + * Returns the type name of the component + */ + std::string GetTypeName() override; + + /** + * Keeps the horizontal layout direction + */ + void SetDirection(EDirection p_direction) override; + + protected: + bool IsDirectionEditable() const override; + }; +} + +namespace OvCore::ECS::Components +{ + template<> + struct ComponentTraits + { + static constexpr std::string_view Name = "class OvCore::ECS::Components::UI::CHorizontalLayout"; + }; +} + diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/CImage.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/CImage.h new file mode 100644 index 000000000..71dde3942 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CImage.h @@ -0,0 +1,128 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI +{ + /** + * Represents a renderable user interface image + */ + class CImage : public AComponent + { + public: + /** + * Constructor + * @param p_owner + */ + CImage(ECS::Actor& p_owner); + + /** + * Returns the name of the component + */ + std::string GetName() override; + + /** + * Returns the type name of the component + */ + virtual std::string GetTypeName() override; + + /** + * Sets the texture rendered by the image + * @param p_texture + */ + void SetTexture(OvRendering::Resources::Texture* p_texture); + + /** + * Returns the texture rendered by the image + */ + OvRendering::Resources::Texture* GetTexture() const; + + /** + * Sets the image size + * @param p_size + */ + void SetSize(const OvMaths::FVector2& p_size); + + /** + * Returns the image size + */ + const OvMaths::FVector2& GetSize() const; + + /** + * Sets the image tint + * @param p_tint + */ + void SetTint(const OvMaths::FVector4& p_tint); + + /** + * Returns the image tint + */ + const OvMaths::FVector4& GetTint() const; + + /** + * Returns the generated quad mesh + */ + OvRendering::Resources::Mesh& GetMesh() const; + + /** + * Returns the generated UI image material, or nullptr if it cannot be initialized + */ + OvCore::Resources::Material* GetMaterial(); + + /** + * Serialize the component + * @param p_doc + * @param p_node + */ + virtual void OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Deserialize the component + * @param p_doc + * @param p_node + */ + virtual void OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Defines how the component should be drawn in the inspector + * @param p_root + */ + virtual void OnInspector(OvUI::Internal::WidgetContainer& p_root) override; + + private: + void RebuildMesh(); + void RefreshMaterial(); + + private: + OvRendering::Resources::Texture* m_texture = nullptr; + OvMaths::FVector2 m_size = { 100.0f, 100.0f }; + OvMaths::FVector4 m_tint = { 1.0f, 1.0f, 1.0f, 1.0f }; + + std::unique_ptr m_mesh; + std::unique_ptr m_material; + }; +} + +namespace OvCore::ECS::Components +{ + template<> + struct ComponentTraits + { + static constexpr std::string_view Name = "class OvCore::ECS::Components::UI::CImage"; + }; +} diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/CLayoutGroup.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/CLayoutGroup.h new file mode 100644 index 000000000..6d869f412 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CLayoutGroup.h @@ -0,0 +1,210 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI +{ + /** + * Arranges direct user interface children along an axis + */ + class CLayoutGroup : public AComponent + { + public: + using ChildOffset = std::pair; + + enum class EDirection + { + HORIZONTAL, + VERTICAL + }; + + enum class EHorizontalAlignment + { + LEFT, + CENTER, + RIGHT + }; + + enum class EVerticalAlignment + { + TOP, + CENTER, + BOTTOM + }; + + /** + * Constructor + * @param p_owner + */ + CLayoutGroup(ECS::Actor& p_owner); + + /** + * Returns the name of the component + */ + std::string GetName() override; + + /** + * Returns the type name of the component + */ + virtual std::string GetTypeName() override; + + /** + * Sets the layout direction + * @param p_direction + */ + virtual void SetDirection(EDirection p_direction); + + /** + * Returns the layout direction + */ + EDirection GetDirection() const; + + /** + * Sets the non-negative spacing between children + * @param p_spacing + */ + void SetSpacing(float p_spacing); + + /** + * Returns the non-negative spacing between children + */ + float GetSpacing() const; + + /** + * Sets the minimum layout container size + * @param p_size + */ + void SetSize(const OvMaths::FVector2& p_size); + + /** + * Returns the minimum layout container size + */ + const OvMaths::FVector2& GetSize() const; + + /** + * Sets the layout padding as left, right, top, bottom + * @param p_padding + */ + void SetPadding(const OvMaths::FVector4& p_padding); + + /** + * Returns the layout padding as left, right, top, bottom + */ + const OvMaths::FVector4& GetPadding() const; + + /** + * Sets the horizontal children alignment + * @param p_alignment + */ + void SetHorizontalAlignment(EHorizontalAlignment p_alignment); + + /** + * Returns the horizontal children alignment + */ + EHorizontalAlignment GetHorizontalAlignment() const; + + /** + * Sets the vertical children alignment + * @param p_alignment + */ + void SetVerticalAlignment(EVerticalAlignment p_alignment); + + /** + * Returns the vertical children alignment + */ + EVerticalAlignment GetVerticalAlignment() const; + + /** + * Sets whether the layout should control children width + * @param p_controlChildrenWidth + */ + void SetControlChildrenWidth(bool p_controlChildrenWidth); + + /** + * Returns whether the layout controls children width + */ + bool GetControlChildrenWidth() const; + + /** + * Sets whether the layout should control children height + * @param p_controlChildrenHeight + */ + void SetControlChildrenHeight(bool p_controlChildrenHeight); + + /** + * Returns whether the layout controls children height + */ + bool GetControlChildrenHeight() const; + + /** + * Returns the layout offset for a direct child + * @param p_child + */ + OvMaths::FVector2 GetChildOffset(const ECS::Actor& p_child) const; + + /** + * Returns the layout offsets for direct children + */ + std::vector GetChildOffsets() const; + + /** + * Applies controlled width/height to direct children using current layout settings + */ + void ApplyControlledChildrenSizes(); + + /** + * Serialize the component + * @param p_doc + * @param p_node + */ + virtual void OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Deserialize the component + * @param p_doc + * @param p_node + */ + virtual void OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Defines how the component should be drawn in the inspector + * @param p_root + */ + virtual void OnInspector(OvUI::Internal::WidgetContainer& p_root) override; + + protected: + virtual bool IsDirectionEditable() const; + + private: + EDirection m_direction = EDirection::HORIZONTAL; + float m_spacing = 0.0f; + OvMaths::FVector2 m_size = OvMaths::FVector2::Zero; + OvMaths::FVector4 m_padding = OvMaths::FVector4::Zero; + EHorizontalAlignment m_horizontalAlignment = EHorizontalAlignment::CENTER; + EVerticalAlignment m_verticalAlignment = EVerticalAlignment::CENTER; + bool m_controlChildrenWidth = false; + bool m_controlChildrenHeight = false; + }; +} + +namespace OvCore::ECS::Components +{ + template<> + struct ComponentTraits + { + static constexpr std::string_view Name = "class OvCore::ECS::Components::UI::CLayoutGroup"; + }; +} diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/CText.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/CText.h new file mode 100644 index 000000000..3477bb6a7 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CText.h @@ -0,0 +1,200 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI +{ + /** + * Represents a renderable user interface text + */ + class CText : public AComponent + { + public: + enum class EHorizontalAlignment + { + LEFT, + CENTER, + RIGHT + }; + + enum class EVerticalAlignment + { + TOP, + CENTER, + BOTTOM + }; + + /** + * Constructor + * @param p_owner + */ + CText(ECS::Actor& p_owner); + + /** + * Returns the name of the component + */ + std::string GetName() override; + + /** + * Returns the type name of the component + */ + virtual std::string GetTypeName() override; + + /** + * Sets the text content + * @param p_text + */ + void SetText(const std::string& p_text); + + /** + * Returns the text content + */ + const std::string& GetText() const; + + /** + * Sets the font resource path + * @param p_fontPath + */ + void SetFontPath(const std::string& p_fontPath); + + /** + * Returns the font resource path + */ + const std::string& GetFontPath() const; + + /** + * Sets the font size in canvas pixels + * @param p_fontSize + */ + void SetFontSize(float p_fontSize); + + /** + * Returns the font size in canvas pixels + */ + float GetFontSize() const; + + /** + * Sets the text color + * @param p_color + */ + void SetColor(const OvMaths::FVector4& p_color); + + /** + * Returns the text color + */ + const OvMaths::FVector4& GetColor() const; + + /** + * Sets the text extents in canvas pixels + * @param p_extents + */ + void SetExtents(const OvMaths::FVector2& p_extents); + + /** + * Returns the text extents in canvas pixels + */ + const OvMaths::FVector2& GetExtents() const; + + /** + * Sets the horizontal text alignment + * @param p_alignment + */ + void SetHorizontalAlignment(EHorizontalAlignment p_alignment); + + /** + * Returns the horizontal text alignment + */ + EHorizontalAlignment GetHorizontalAlignment() const; + + /** + * Sets the vertical text alignment + * @param p_alignment + */ + void SetVerticalAlignment(EVerticalAlignment p_alignment); + + /** + * Returns the vertical text alignment + */ + EVerticalAlignment GetVerticalAlignment() const; + + /** + * Returns the generated text mesh, or nullptr if the text cannot be rendered + */ + OvRendering::Resources::Mesh* GetMesh() const; + + /** + * Returns the generated text material, or nullptr if it cannot be initialized + */ + OvRendering::Data::Material* GetMaterial(); + + /** + * Returns the generated text bounds size + */ + const OvMaths::FVector2& GetSize() const; + + /** + * Serialize the component + * @param p_doc + * @param p_node + */ + virtual void OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Deserialize the component + * @param p_doc + * @param p_node + */ + virtual void OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Defines how the component should be drawn in the inspector + * @param p_root + */ + virtual void OnInspector(OvUI::Internal::WidgetContainer& p_root) override; + + private: + OvRendering::Resources::Font* GetFont() const; + void MarkMeshDirty(); + void RebuildMesh() const; + void RefreshMaterial(); + + private: + std::string m_text = "Text"; + std::string m_fontPath = ":Fonts\\Roboto-Regular.ttf"; + float m_fontSize = 32.0f; + OvMaths::FVector4 m_color = { 1.0f, 1.0f, 1.0f, 1.0f }; + OvMaths::FVector2 m_extents = OvMaths::FVector2::Zero; + EHorizontalAlignment m_horizontalAlignment = EHorizontalAlignment::LEFT; + EVerticalAlignment m_verticalAlignment = EVerticalAlignment::TOP; + + mutable bool m_meshDirty = true; + mutable OvMaths::FVector2 m_size = OvMaths::FVector2::Zero; + mutable std::unique_ptr m_mesh; + std::unique_ptr m_material; + }; +} + +namespace OvCore::ECS::Components +{ + template<> + struct ComponentTraits + { + static constexpr std::string_view Name = "class OvCore::ECS::Components::UI::CText"; + }; +} diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/CTransform2D.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/CTransform2D.h new file mode 100644 index 000000000..ed9866ff8 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CTransform2D.h @@ -0,0 +1,176 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI +{ + /** + * Represents 2D transformations applied to a user interface actor + */ + class CTransform2D : public AComponent + { + public: + enum class EAnchorPreset + { + TOP_LEFT, + TOP_CENTER, + TOP_RIGHT, + MIDDLE_LEFT, + CENTER, + MIDDLE_RIGHT, + BOTTOM_LEFT, + BOTTOM_CENTER, + BOTTOM_RIGHT, + HORIZONTAL_STRETCH_TOP, + HORIZONTAL_STRETCH_MIDDLE, + HORIZONTAL_STRETCH_BOTTOM, + VERTICAL_STRETCH_LEFT, + VERTICAL_STRETCH_CENTER, + VERTICAL_STRETCH_RIGHT, + STRETCH_BOTH + }; + + /** + * Constructor + * @param p_owner + */ + CTransform2D(ECS::Actor& p_owner); + + /** + * Returns the name of the component + */ + std::string GetName() override; + + /** + * Returns the type name of the component + */ + virtual std::string GetTypeName() override; + + /** + * Sets the 2D position + * @param p_position + */ + void SetPosition(const OvMaths::FVector2& p_position); + + /** + * Returns the 2D position + */ + const OvMaths::FVector2& GetPosition() const; + + /** + * Sets the 2D rotation in degrees + * @param p_rotation + */ + void SetRotation(float p_rotation); + + /** + * Returns the 2D rotation in degrees + */ + float GetRotation() const; + + /** + * Sets the 2D scale + * @param p_scale + */ + void SetScale(const OvMaths::FVector2& p_scale); + + /** + * Returns the 2D scale + */ + OvMaths::FVector2 GetScale() const; + + /** + * Sets the anchor preset + * @param p_anchorPreset + */ + void SetAnchorPreset(EAnchorPreset p_anchorPreset); + + /** + * Returns the anchor preset + */ + EAnchorPreset GetAnchorPreset() const; + + /** + * Sets the local 2D size in canvas pixels + * @param p_size + */ + void SetSize(const OvMaths::FVector2& p_size); + + /** + * Returns the local 2D size in canvas pixels + */ + const OvMaths::FVector2& GetSize() const; + + /** + * Sets the normalized pivot in range [-1, 1] on each axis + * @param p_pivot + */ + void SetPivot(const OvMaths::FVector2& p_pivot); + + /** + * Returns the normalized pivot in range [-1, 1] on each axis + */ + const OvMaths::FVector2& GetPivot() const; + + /** + * Returns a transform matrix resolved against a canvas size and layout offset + * @param p_canvasSize + * @param p_layoutOffset + * @param p_elementSize + */ + OvMaths::FMatrix4 GetMatrix( + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset = OvMaths::FVector2::Zero, + const OvMaths::FVector2& p_elementSize = OvMaths::FVector2::Zero + ) const; + + /** + * Serialize the component + * @param p_doc + * @param p_node + */ + virtual void OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Deserialize the component + * @param p_doc + * @param p_node + */ + virtual void OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Defines how the component should be drawn in the inspector + * @param p_root + */ + virtual void OnInspector(OvUI::Internal::WidgetContainer& p_root) override; + + private: + OvMaths::FVector2 GetAnchoredPosition(const OvMaths::FVector2& p_canvasSize, const OvMaths::FVector2& p_layoutOffset) const; + OvMaths::FVector2 GetEffectiveSize(const OvMaths::FVector2& p_elementSize) const; + + private: + OvMaths::FVector2 m_position = OvMaths::FVector2::Zero; + OvMaths::FVector2 m_size = OvMaths::FVector2::Zero; + OvMaths::FVector2 m_pivot = OvMaths::FVector2::Zero; + EAnchorPreset m_anchorPreset = EAnchorPreset::CENTER; + }; +} + +namespace OvCore::ECS::Components +{ + template<> + struct ComponentTraits + { + static constexpr std::string_view Name = "class OvCore::ECS::Components::UI::CTransform2D"; + }; +} diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/CVerticalLayout.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/CVerticalLayout.h new file mode 100644 index 000000000..6d8a65c6f --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CVerticalLayout.h @@ -0,0 +1,53 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +namespace OvCore::ECS::Components::UI +{ + /** + * Arranges direct user interface children vertically + */ + class CVerticalLayout : public CLayoutGroup + { + public: + /** + * Constructor + * @param p_owner + */ + CVerticalLayout(ECS::Actor& p_owner); + + /** + * Returns the name of the component + */ + std::string GetName() override; + + /** + * Returns the type name of the component + */ + std::string GetTypeName() override; + + /** + * Keeps the vertical layout direction + */ + void SetDirection(EDirection p_direction) override; + + protected: + bool IsDirectionEditable() const override; + }; +} + +namespace OvCore::ECS::Components +{ + template<> + struct ComponentTraits + { + static constexpr std::string_view Name = "class OvCore::ECS::Components::UI::CVerticalLayout"; + }; +} + diff --git a/Sources/OvCore/include/OvCore/Rendering/EngineDrawableDescriptor.h b/Sources/OvCore/include/OvCore/Rendering/EngineDrawableDescriptor.h index 5188bc879..3961523ee 100644 --- a/Sources/OvCore/include/OvCore/Rendering/EngineDrawableDescriptor.h +++ b/Sources/OvCore/include/OvCore/Rendering/EngineDrawableDescriptor.h @@ -6,12 +6,14 @@ #pragma once +#include + #include namespace OvCore::Rendering { /** - * Descriptor for drawable entities that adds a model and a user matrix. + * Descriptor for drawable entities that adds engine matrices. * This descriptor, when added on a drawable, is read by the EngineBufferRenderFeature * and its data is uploaded to the GPU before issuing a draw call. */ @@ -19,5 +21,7 @@ namespace OvCore::Rendering { OvMaths::FMatrix4 modelMatrix; OvMaths::FMatrix4 userMatrix; + std::optional viewMatrixOverride; + std::optional projectionMatrixOverride; }; } diff --git a/Sources/OvCore/include/OvCore/Rendering/SceneRenderer.h b/Sources/OvCore/include/OvCore/Rendering/SceneRenderer.h index 97f3e1066..c133080c8 100644 --- a/Sources/OvCore/include/OvCore/Rendering/SceneRenderer.h +++ b/Sources/OvCore/include/OvCore/Rendering/SceneRenderer.h @@ -7,8 +7,11 @@ #pragma once #include +#include +#include #include +#include #include #include #include @@ -39,7 +42,7 @@ namespace OvCore::Rendering struct DrawOrder { const int order; - const uintptr_t materialKey; + const OvRendering::Data::Material* materialKey; const float distance; /** @@ -86,11 +89,15 @@ namespace OvCore::Rendering OvTools::Utils::OptRef frustumOverride; OvTools::Utils::OptRef overrideMaterial; OvTools::Utils::OptRef fallbackMaterial; + bool includeUI = true; + bool renderUIInScreenSpace = true; }; struct SceneParsingInput { OvCore::SceneSystem::Scene& scene; + OvMaths::FVector2 renderSize = { 1.0f, 1.0f }; + bool renderUIInScreenSpace = true; }; /** @@ -109,6 +116,8 @@ namespace OvCore::Rendering OvCore::ECS::Actor& actor; EVisibilityFlags visibilityFlags = EVisibilityFlags::NONE; std::optional bounds; + std::optional drawOrderOverride; + bool isUserInterface = false; }; /** diff --git a/Sources/OvCore/include/OvCore/Rendering/UIRenderingUtils.h b/Sources/OvCore/include/OvCore/Rendering/UIRenderingUtils.h new file mode 100644 index 000000000..a78347e5d --- /dev/null +++ b/Sources/OvCore/include/OvCore/Rendering/UIRenderingUtils.h @@ -0,0 +1,41 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +namespace OvCore::ECS { class Actor; } +namespace OvCore::ECS::Components::UI { class CCanvas; } + +namespace OvCore::Rendering::UIRenderingUtils +{ + OvMaths::FVector2 ClampCanvasSize(const OvMaths::FVector2& p_canvasSize); + + OvMaths::FVector2 GetCanvasSize( + const OvCore::ECS::Components::UI::CCanvas& p_canvas, + const OvMaths::FVector2& p_renderSize + ); + + float GetCanvasScale( + const OvCore::ECS::Components::UI::CCanvas& p_canvas, + const OvMaths::FVector2& p_renderSize + ); + + const OvCore::ECS::Components::UI::CCanvas* FindCanvas(const OvCore::ECS::Actor& p_owner); + + OvMaths::FVector2 GetCanvasSize( + const OvCore::ECS::Actor& p_owner, + const OvMaths::FVector2& p_renderSize + ); + + OvMaths::FVector2 GetLayoutOffset(const OvCore::ECS::Actor& p_owner); + + float GetUIWorldScale( + const OvCore::ECS::Components::UI::CCanvas& p_canvas, + bool p_screenSpace + ); +} diff --git a/Sources/OvCore/include/OvCore/ResourceManagement/FontManager.h b/Sources/OvCore/include/OvCore/ResourceManagement/FontManager.h new file mode 100644 index 000000000..76bf6478f --- /dev/null +++ b/Sources/OvCore/include/OvCore/ResourceManagement/FontManager.h @@ -0,0 +1,40 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +#include "OvCore/ResourceManagement/AResourceManager.h" + +namespace OvCore::ResourceManagement +{ + /** + * ResourceManager of fonts + */ + class FontManager : public AResourceManager + { + public: + /** + * Create the resource identified by the given path + * @param p_path + */ + virtual OvRendering::Resources::Font* CreateResource(const std::filesystem::path& p_path) override; + + /** + * Destroy the given resource + * @param p_resource + */ + virtual void DestroyResource(OvRendering::Resources::Font* p_resource) override; + + /** + * Reload the given resource + * @param p_resource + * @param p_path + */ + virtual void ReloadResource(OvRendering::Resources::Font* p_resource, const std::filesystem::path& p_path) override; + }; +} diff --git a/Sources/OvCore/premake5.lua b/Sources/OvCore/premake5.lua index 5e9b8f592..57d2012ef 100644 --- a/Sources/OvCore/premake5.lua +++ b/Sources/OvCore/premake5.lua @@ -23,6 +23,7 @@ project "OvCore" -- Dependencies dependdir .. "glad/include", dependdir .. "ImGui/include", + dependdir .. "clay/include", dependdir .. "lua/include", dependdir .. "sol/include", dependdir .. "tinyxml2/include", diff --git a/Sources/OvCore/src/OvCore/ECS/Actor.cpp b/Sources/OvCore/src/OvCore/ECS/Actor.cpp index 332b73688..b7667f864 100644 --- a/Sources/OvCore/src/OvCore/ECS/Actor.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Actor.cpp @@ -25,6 +25,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #include @@ -606,6 +613,13 @@ void OvCore::ECS::Actor::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2:: else if (IsType(componentType)) component = &AddComponent(); else if (IsType(componentType)) component = &AddComponent(); else if (IsType(componentType)) component = &AddComponent(); + else if (IsType(componentType)) component = &AddComponent(); + else if (IsType(componentType)) component = &AddComponent(); + else if (IsType(componentType)) component = &AddComponent(); + else if (IsType(componentType)) component = &AddComponent(); + else if (IsType(componentType)) component = &AddComponent(); + else if (IsType(componentType)) component = &AddComponent(); + else if (IsType(componentType)) component = &AddComponent(); if (component) { diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CTransform.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CTransform.cpp index 79ddff68b..97fc7d5fe 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CTransform.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CTransform.cpp @@ -5,6 +5,8 @@ */ #include +#include +#include OvCore::ECS::Components::CTransform::CTransform(ECS::Actor& p_owner, OvMaths::FVector3 p_localPosition, OvMaths::FQuaternion p_localRotation, OvMaths::FVector3 p_localScale) : AComponent(p_owner) @@ -186,7 +188,20 @@ void OvCore::ECS::Components::CTransform::OnInspector(OvUI::Internal::WidgetCont SetLocalRotation(OvMaths::FQuaternion(result)); }; - OvCore::Helpers::GUIDrawer::DrawVec3(p_root, "Position", std::bind(&CTransform::GetLocalPosition, this), std::bind(&CTransform::SetLocalPosition, this, std::placeholders::_1), 0.05f); + const bool hasTransform2D = owner.GetComponent() != nullptr; + if (hasTransform2D) + { + OvCore::Helpers::GUIDrawer::DrawReadOnlyString( + p_root, + "Position", + []() { return std::string("Driven by Transform 2D (anchored px)"); } + ); + } + else + { + OvCore::Helpers::GUIDrawer::DrawVec3(p_root, "Position", std::bind(&CTransform::GetLocalPosition, this), std::bind(&CTransform::SetLocalPosition, this, std::placeholders::_1), 0.05f); + } + OvCore::Helpers::GUIDrawer::DrawVec3(p_root, "Rotation", getRotation, setRotation, 0.05f); OvCore::Helpers::GUIDrawer::DrawVec3(p_root, "Scale", std::bind(&CTransform::GetLocalScale, this), std::bind(&CTransform::SetLocalScale, this, std::placeholders::_1), 0.05f, 0.0001f); } diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/CCanvas.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/CCanvas.cpp new file mode 100644 index 000000000..03326365d --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CCanvas.cpp @@ -0,0 +1,305 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +namespace +{ + constexpr float kMinimumReferenceResolutionAxis = 1.0f; + constexpr float kMinimumScaleFactor = 0.0001f; + constexpr float kMinimumPixelsPerUnit = 0.0001f; + constexpr float kMinimumMatchWidthOrHeight = 0.0f; + constexpr float kMaximumMatchWidthOrHeight = 1.0f; + + float ClampFinite(float p_value, float p_min) + { + return std::isfinite(p_value) ? std::max(p_value, p_min) : p_min; + } + + float ClampFiniteNormalized(float p_value, float p_fallback) + { + if (!std::isfinite(p_value)) + { + return p_fallback; + } + + return std::clamp(p_value, kMinimumMatchWidthOrHeight, kMaximumMatchWidthOrHeight); + } + + OvCore::ECS::Components::UI::CCanvas::EScalerMode ToScalerMode(int p_value) + { + using EScalerMode = OvCore::ECS::Components::UI::CCanvas::EScalerMode; + + switch (p_value) + { + case static_cast(EScalerMode::SCALE_WITH_SCREEN_SIZE): + return EScalerMode::SCALE_WITH_SCREEN_SIZE; + case static_cast(EScalerMode::CONSTANT_PIXEL_SIZE): + default: + return EScalerMode::CONSTANT_PIXEL_SIZE; + } + } + + OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode ToScreenMatchMode(int p_value) + { + using EScreenMatchMode = OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode; + + switch (p_value) + { + case static_cast(EScreenMatchMode::EXPAND): + return EScreenMatchMode::EXPAND; + case static_cast(EScreenMatchMode::SHRINK): + return EScreenMatchMode::SHRINK; + case static_cast(EScreenMatchMode::MATCH_WIDTH_OR_HEIGHT): + default: + return EScreenMatchMode::MATCH_WIDTH_OR_HEIGHT; + } + } + + void RebuildScaleWithScreenSettings( + OvCore::ECS::Components::UI::CCanvas& p_canvas, + OvUI::Internal::WidgetContainer& p_root + ) + { + p_root.RemoveAllWidgets(); + + if (p_canvas.GetScalerMode() != OvCore::ECS::Components::UI::CCanvas::EScalerMode::SCALE_WITH_SCREEN_SIZE) + { + return; + } + + OvCore::Helpers::GUIDrawer::CreateTitle(p_root, "Screen Match Mode"); + auto& screenMatchMode = p_root.CreateWidget(static_cast(p_canvas.GetScreenMatchMode())); + screenMatchMode.choices.emplace( + static_cast(OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode::MATCH_WIDTH_OR_HEIGHT), + "Match Width Or Height" + ); + screenMatchMode.choices.emplace( + static_cast(OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode::EXPAND), + "Expand" + ); + screenMatchMode.choices.emplace( + static_cast(OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode::SHRINK), + "Shrink" + ); + + auto* canvas = &p_canvas; + auto& screenMatchModeDispatcher = screenMatchMode.AddPlugin>(); + screenMatchModeDispatcher.RegisterGatherer([canvas]() { return static_cast(canvas->GetScreenMatchMode()); }); + screenMatchModeDispatcher.RegisterProvider([canvas](int p_choice) { canvas->SetScreenMatchMode(ToScreenMatchMode(p_choice)); }); + + auto* scaleWithScreenSettings = &p_root; + screenMatchMode.ValueChangedEvent += [canvas, scaleWithScreenSettings](int p_choice) + { + canvas->SetScreenMatchMode(ToScreenMatchMode(p_choice)); + RebuildScaleWithScreenSettings(*canvas, *scaleWithScreenSettings); + }; + + if (p_canvas.GetScreenMatchMode() == OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode::MATCH_WIDTH_OR_HEIGHT) + { + OvCore::Helpers::GUIDrawer::DrawScalar( + p_root, + "Match Width Or Height", + [canvas]() { return canvas->GetMatchWidthOrHeight(); }, + [canvas](float p_value) { canvas->SetMatchWidthOrHeight(p_value); }, + 0.01f, + kMinimumMatchWidthOrHeight, + kMaximumMatchWidthOrHeight + ); + } + } +} + +OvCore::ECS::Components::UI::CCanvas::CCanvas(ECS::Actor& p_owner) : +AComponent(p_owner) +{ +} + +std::string OvCore::ECS::Components::UI::CCanvas::GetName() +{ + return "Canvas"; +} + +std::string OvCore::ECS::Components::UI::CCanvas::GetTypeName() +{ + return std::string{ComponentTraits::Name}; +} + +void OvCore::ECS::Components::UI::CCanvas::SetReferenceResolution(const OvMaths::FVector2& p_referenceResolution) +{ + m_referenceResolution.x = ClampFinite(p_referenceResolution.x, kMinimumReferenceResolutionAxis); + m_referenceResolution.y = ClampFinite(p_referenceResolution.y, kMinimumReferenceResolutionAxis); +} + +const OvMaths::FVector2& OvCore::ECS::Components::UI::CCanvas::GetReferenceResolution() const +{ + return m_referenceResolution; +} + +void OvCore::ECS::Components::UI::CCanvas::SetScaleFactor(float p_scaleFactor) +{ + m_scaleFactor = ClampFinite(p_scaleFactor, kMinimumScaleFactor); +} + +float OvCore::ECS::Components::UI::CCanvas::GetScaleFactor() const +{ + return m_scaleFactor; +} + +void OvCore::ECS::Components::UI::CCanvas::SetPixelsPerUnit(float p_pixelsPerUnit) +{ + m_pixelsPerUnit = ClampFinite(p_pixelsPerUnit, kMinimumPixelsPerUnit); +} + +float OvCore::ECS::Components::UI::CCanvas::GetPixelsPerUnit() const +{ + return m_pixelsPerUnit; +} + +void OvCore::ECS::Components::UI::CCanvas::SetScalerMode(EScalerMode p_scalerMode) +{ + m_scalerMode = ToScalerMode(static_cast(p_scalerMode)); +} + +OvCore::ECS::Components::UI::CCanvas::EScalerMode OvCore::ECS::Components::UI::CCanvas::GetScalerMode() const +{ + return m_scalerMode; +} + +void OvCore::ECS::Components::UI::CCanvas::SetScreenMatchMode(EScreenMatchMode p_screenMatchMode) +{ + m_screenMatchMode = ToScreenMatchMode(static_cast(p_screenMatchMode)); +} + +OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode OvCore::ECS::Components::UI::CCanvas::GetScreenMatchMode() const +{ + return m_screenMatchMode; +} + +void OvCore::ECS::Components::UI::CCanvas::SetMatchWidthOrHeight(float p_matchWidthOrHeight) +{ + m_matchWidthOrHeight = ClampFiniteNormalized(p_matchWidthOrHeight, m_matchWidthOrHeight); +} + +float OvCore::ECS::Components::UI::CCanvas::GetMatchWidthOrHeight() const +{ + return m_matchWidthOrHeight; +} + +void OvCore::ECS::Components::UI::CCanvas::OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + Helpers::Serializer::SerializeVec2(p_doc, p_node, "reference_resolution", m_referenceResolution); + Helpers::Serializer::SerializeFloat(p_doc, p_node, "scale_factor", m_scaleFactor); + Helpers::Serializer::SerializeFloat(p_doc, p_node, "pixels_per_unit", m_pixelsPerUnit); + Helpers::Serializer::SerializeInt(p_doc, p_node, "scaler_mode", static_cast(m_scalerMode)); + Helpers::Serializer::SerializeInt(p_doc, p_node, "screen_match_mode", static_cast(m_screenMatchMode)); + Helpers::Serializer::SerializeFloat(p_doc, p_node, "match_width_or_height", m_matchWidthOrHeight); +} + +void OvCore::ECS::Components::UI::CCanvas::OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + if (p_node->FirstChildElement("reference_resolution")) + { + auto referenceResolution = m_referenceResolution; + Helpers::Serializer::DeserializeVec2(p_doc, p_node, "reference_resolution", referenceResolution); + SetReferenceResolution(referenceResolution); + } + + if (p_node->FirstChildElement("scale_factor")) + { + auto scaleFactor = m_scaleFactor; + Helpers::Serializer::DeserializeFloat(p_doc, p_node, "scale_factor", scaleFactor); + SetScaleFactor(scaleFactor); + } + + if (p_node->FirstChildElement("pixels_per_unit")) + { + auto pixelsPerUnit = m_pixelsPerUnit; + Helpers::Serializer::DeserializeFloat(p_doc, p_node, "pixels_per_unit", pixelsPerUnit); + SetPixelsPerUnit(pixelsPerUnit); + } + + if (p_node->FirstChildElement("scaler_mode")) + { + auto scalerMode = static_cast(m_scalerMode); + Helpers::Serializer::DeserializeInt(p_doc, p_node, "scaler_mode", scalerMode); + SetScalerMode(ToScalerMode(scalerMode)); + } + + if (p_node->FirstChildElement("screen_match_mode")) + { + auto screenMatchMode = static_cast(m_screenMatchMode); + Helpers::Serializer::DeserializeInt(p_doc, p_node, "screen_match_mode", screenMatchMode); + SetScreenMatchMode(ToScreenMatchMode(screenMatchMode)); + } + + if (p_node->FirstChildElement("match_width_or_height")) + { + auto matchWidthOrHeight = m_matchWidthOrHeight; + Helpers::Serializer::DeserializeFloat(p_doc, p_node, "match_width_or_height", matchWidthOrHeight); + SetMatchWidthOrHeight(matchWidthOrHeight); + } +} + +void OvCore::ECS::Components::UI::CCanvas::OnInspector(OvUI::Internal::WidgetContainer& p_root) +{ + Helpers::GUIDrawer::DrawVec2( + p_root, + "Reference Resolution", + [this]() { return GetReferenceResolution(); }, + [this](OvMaths::FVector2 p_value) { SetReferenceResolution(p_value); }, + 1.0f, + kMinimumReferenceResolutionAxis + ); + + Helpers::GUIDrawer::DrawScalar( + p_root, + "Scale Factor", + std::bind(&CCanvas::GetScaleFactor, this), + std::bind(&CCanvas::SetScaleFactor, this, std::placeholders::_1), + 0.01f, + kMinimumScaleFactor + ); + + Helpers::GUIDrawer::DrawScalar( + p_root, + "Pixels Per Unit", + std::bind(&CCanvas::GetPixelsPerUnit, this), + std::bind(&CCanvas::SetPixelsPerUnit, this, std::placeholders::_1), + 1.0f, + kMinimumPixelsPerUnit + ); + + Helpers::GUIDrawer::CreateTitle(p_root, "Scaler Mode"); + auto& scalerMode = p_root.CreateWidget(static_cast(GetScalerMode())); + scalerMode.choices.emplace(static_cast(EScalerMode::CONSTANT_PIXEL_SIZE), "Constant Pixel Size"); + scalerMode.choices.emplace(static_cast(EScalerMode::SCALE_WITH_SCREEN_SIZE), "Scale With Screen Size"); + auto& scalerModeDispatcher = scalerMode.AddPlugin>(); + scalerModeDispatcher.RegisterGatherer([this]() { return static_cast(GetScalerMode()); }); + scalerModeDispatcher.RegisterProvider([this](int p_choice) { SetScalerMode(ToScalerMode(p_choice)); }); + + auto& scaleWithScreenSettings = p_root.CreateWidget(); + auto* scaleWithScreenSettingsWidget = &scaleWithScreenSettings; + auto* canvas = this; + scalerMode.ValueChangedEvent += [canvas, scaleWithScreenSettingsWidget](int p_choice) + { + canvas->SetScalerMode(ToScalerMode(p_choice)); + RebuildScaleWithScreenSettings(*canvas, *scaleWithScreenSettingsWidget); + }; + + RebuildScaleWithScreenSettings(*this, scaleWithScreenSettings); +} diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/CHorizontalLayout.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/CHorizontalLayout.cpp new file mode 100644 index 000000000..98a95fa59 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CHorizontalLayout.cpp @@ -0,0 +1,33 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include + +OvCore::ECS::Components::UI::CHorizontalLayout::CHorizontalLayout(ECS::Actor& p_owner) : + CLayoutGroup(p_owner) +{ + SetDirection(EDirection::HORIZONTAL); +} + +std::string OvCore::ECS::Components::UI::CHorizontalLayout::GetName() +{ + return "Horizontal Layout"; +} + +std::string OvCore::ECS::Components::UI::CHorizontalLayout::GetTypeName() +{ + return std::string{ ComponentTraits::Name }; +} + +void OvCore::ECS::Components::UI::CHorizontalLayout::SetDirection(EDirection) +{ + CLayoutGroup::SetDirection(EDirection::HORIZONTAL); +} + +bool OvCore::ECS::Components::UI::CHorizontalLayout::IsDirectionEditable() const +{ + return false; +} diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/CImage.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/CImage.cpp new file mode 100644 index 000000000..3d4a4c90c --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CImage.cpp @@ -0,0 +1,233 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr float kMinimumSize = 0.0001f; + constexpr const char* kDefaultMaterialPath = ":Materials\\UI_Image.ovmat"; + constexpr const char* kTextureUniform = "u_Image"; + constexpr const char* kTintUniform = "u_Tint"; + + float ClampFinite(float p_value, float p_min) + { + return std::isfinite(p_value) ? std::max(p_value, p_min) : p_min; + } + + float KeepFinite(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? p_value : p_fallback; + } + + OvUI::Types::Color ToColor(const OvMaths::FVector4& p_value) + { + return { p_value.x, p_value.y, p_value.z, p_value.w }; + } + + OvMaths::FVector4 ToVec4(const OvUI::Types::Color& p_value) + { + return { p_value.r, p_value.g, p_value.b, p_value.a }; + } +} + +OvCore::ECS::Components::UI::CImage::CImage(ECS::Actor& p_owner) : +AComponent(p_owner) +{ + RebuildMesh(); +} + +std::string OvCore::ECS::Components::UI::CImage::GetName() +{ + return "Image"; +} + +std::string OvCore::ECS::Components::UI::CImage::GetTypeName() +{ + return std::string{ComponentTraits::Name}; +} + +void OvCore::ECS::Components::UI::CImage::SetTexture(OvRendering::Resources::Texture* p_texture) +{ + m_texture = p_texture; + RefreshMaterial(); +} + +OvRendering::Resources::Texture* OvCore::ECS::Components::UI::CImage::GetTexture() const +{ + return m_texture; +} + +void OvCore::ECS::Components::UI::CImage::SetSize(const OvMaths::FVector2& p_size) +{ + m_size.x = ClampFinite(p_size.x, kMinimumSize); + m_size.y = ClampFinite(p_size.y, kMinimumSize); + RebuildMesh(); +} + +const OvMaths::FVector2& OvCore::ECS::Components::UI::CImage::GetSize() const +{ + return m_size; +} + +void OvCore::ECS::Components::UI::CImage::SetTint(const OvMaths::FVector4& p_tint) +{ + m_tint.x = KeepFinite(p_tint.x, m_tint.x); + m_tint.y = KeepFinite(p_tint.y, m_tint.y); + m_tint.z = KeepFinite(p_tint.z, m_tint.z); + m_tint.w = KeepFinite(p_tint.w, m_tint.w); + RefreshMaterial(); +} + +const OvMaths::FVector4& OvCore::ECS::Components::UI::CImage::GetTint() const +{ + return m_tint; +} + +OvRendering::Resources::Mesh& OvCore::ECS::Components::UI::CImage::GetMesh() const +{ + return *m_mesh; +} + +OvCore::Resources::Material* OvCore::ECS::Components::UI::CImage::GetMaterial() +{ + RefreshMaterial(); + return m_material && m_material->IsValid() ? m_material.get() : nullptr; +} + +void OvCore::ECS::Components::UI::CImage::OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + Helpers::Serializer::SerializeTexture(p_doc, p_node, "texture", m_texture); + Helpers::Serializer::SerializeVec2(p_doc, p_node, "size", m_size); + Helpers::Serializer::SerializeVec4(p_doc, p_node, "tint", m_tint); +} + +void OvCore::ECS::Components::UI::CImage::OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + if (p_node->FirstChildElement("texture")) + { + OvRendering::Resources::Texture* texture = m_texture; + Helpers::Serializer::DeserializeTexture(p_doc, p_node, "texture", texture); + SetTexture(texture); + } + + if (p_node->FirstChildElement("size")) + { + auto size = m_size; + Helpers::Serializer::DeserializeVec2(p_doc, p_node, "size", size); + SetSize(size); + } + + if (p_node->FirstChildElement("tint")) + { + auto tint = m_tint; + Helpers::Serializer::DeserializeVec4(p_doc, p_node, "tint", tint); + SetTint(tint); + } +} + +void OvCore::ECS::Components::UI::CImage::OnInspector(OvUI::Internal::WidgetContainer& p_root) +{ + Helpers::GUIDrawer::DrawTexture(p_root, "Texture", m_texture); + + if (!owner.GetComponent()) + { + Helpers::GUIDrawer::DrawVec2( + p_root, + "Size", + [this]() { return GetSize(); }, + [this](OvMaths::FVector2 p_value) { SetSize(p_value); }, + 1.0f, + kMinimumSize + ); + } + else + { + Helpers::GUIDrawer::DrawReadOnlyString( + p_root, + "Size", + []() { return std::string("Driven by Transform 2D"); } + ); + } + + Helpers::GUIDrawer::DrawColor( + p_root, + "Tint", + [this]() { return ToColor(m_tint); }, + [this](OvUI::Types::Color p_value) { SetTint(ToVec4(p_value)); }, + true + ); +} + +void OvCore::ECS::Components::UI::CImage::RebuildMesh() +{ + const float halfWidth = m_size.x * 0.5f; + const float halfHeight = m_size.y * 0.5f; + + const std::array vertices = { + OvRendering::Geometry::Vertex{{ -halfWidth, -halfHeight, 0.0f }, { 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f }, {}, {}}, + OvRendering::Geometry::Vertex{{ halfWidth, -halfHeight, 0.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f }, {}, {}}, + OvRendering::Geometry::Vertex{{ halfWidth, halfHeight, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 0.0f, 1.0f }, {}, {}}, + OvRendering::Geometry::Vertex{{ -halfWidth, halfHeight, 0.0f }, { 0.0f, 1.0f }, { 0.0f, 0.0f, 1.0f }, {}, {}} + }; + + const std::array indices = { 0, 1, 2, 0, 2, 3 }; + + m_mesh = std::make_unique(vertices, indices); +} + +void OvCore::ECS::Components::UI::CImage::RefreshMaterial() +{ + if (!m_material) + { + m_material = std::make_unique(); + } + + auto* defaultMaterial = Global::ServiceLocator::Get().GetResource(kDefaultMaterialPath); + if (!defaultMaterial || !defaultMaterial->HasShader()) + { + m_material->SetShader(nullptr); + return; + } + + if (m_material->GetShader() != defaultMaterial->GetShader()) + { + m_material->SetShader(defaultMaterial->GetShader()); + } + + m_material->SetOrthographicSupport(true); + m_material->SetPerspectiveSupport(true); + m_material->SetBlendable(true); + m_material->SetUserInterface(true); + m_material->SetBackfaceCulling(false); + m_material->SetFrontfaceCulling(false); + m_material->SetDepthTest(false); + m_material->SetDepthWriting(false); + m_material->SetColorWriting(true); + m_material->SetCastShadows(false); + m_material->SetReceiveShadows(false); + m_material->SetCapturedByReflectionProbes(false); + m_material->SetReceiveReflections(false); + m_material->SetGPUInstances(1); + + m_material->TrySetProperty(kTextureUniform, m_texture); + m_material->TrySetProperty(kTintUniform, m_tint); +} diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/CLayoutGroup.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/CLayoutGroup.cpp new file mode 100644 index 000000000..a5aba51e8 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CLayoutGroup.cpp @@ -0,0 +1,833 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4244 4305) +#endif + +#include + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + struct LayoutChild + { + OvCore::ECS::Actor* actor = nullptr; + OvMaths::FVector2 size; + }; + + constexpr float kMinimumSpacing = 0.0f; + constexpr float kMinimumSize = 0.0f; + constexpr float kMinimumPadding = 0.0f; + constexpr float kMaximumSpacing = static_cast(std::numeric_limits::max()); + constexpr float kMaximumPadding = static_cast(std::numeric_limits::max()); + constexpr float kSizeUpdateEpsilon = 0.0001f; + + float ClampSpacing(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? std::clamp(p_value, kMinimumSpacing, kMaximumSpacing) : p_fallback; + } + + float ClampSize(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? std::max(p_value, kMinimumSize) : p_fallback; + } + + float ClampPadding(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? std::clamp(p_value, kMinimumPadding, kMaximumPadding) : p_fallback; + } + + bool IsNearlyEqual(float p_left, float p_right) + { + return std::abs(p_left - p_right) <= kSizeUpdateEpsilon; + } + + bool ShouldUpdateControlledSize( + const OvMaths::FVector2& p_currentSize, + const OvMaths::FVector2& p_targetSize, + bool p_controlWidth, + bool p_controlHeight + ) + { + if (p_controlWidth && !IsNearlyEqual(p_currentSize.x, p_targetSize.x)) + { + return true; + } + + if (p_controlHeight && !IsNearlyEqual(p_currentSize.y, p_targetSize.y)) + { + return true; + } + + return false; + } + + OvCore::ECS::Components::UI::CLayoutGroup::EDirection ToDirection(int p_value) + { + using EDirection = OvCore::ECS::Components::UI::CLayoutGroup::EDirection; + + switch (p_value) + { + case static_cast(EDirection::VERTICAL): + return EDirection::VERTICAL; + case static_cast(EDirection::HORIZONTAL): + default: + return EDirection::HORIZONTAL; + } + } + + OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment ToHorizontalAlignment(int p_value) + { + using EHorizontalAlignment = OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment; + + switch (p_value) + { + case static_cast(EHorizontalAlignment::LEFT): + return EHorizontalAlignment::LEFT; + case static_cast(EHorizontalAlignment::RIGHT): + return EHorizontalAlignment::RIGHT; + case static_cast(EHorizontalAlignment::CENTER): + default: + return EHorizontalAlignment::CENTER; + } + } + + OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment ToVerticalAlignment(int p_value) + { + using EVerticalAlignment = OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment; + + switch (p_value) + { + case static_cast(EVerticalAlignment::TOP): + return EVerticalAlignment::TOP; + case static_cast(EVerticalAlignment::BOTTOM): + return EVerticalAlignment::BOTTOM; + case static_cast(EVerticalAlignment::CENTER): + default: + return EVerticalAlignment::CENTER; + } + } + + std::optional GetLayoutSize(const OvCore::ECS::Actor& p_child) + { + if (const auto* image = p_child.GetComponent(); image) + { + return image->GetSize(); + } + + if (const auto* text = p_child.GetComponent(); text) + { + return text->GetSize(); + } + + if (const auto* transform2D = p_child.GetComponent(); transform2D) + { + const auto& size = transform2D->GetSize(); + if (size.x > 0.0f && size.y > 0.0f) + { + return size; + } + } + + return std::nullopt; + } + + std::vector CollectLayoutChildren(OvCore::ECS::Actor& p_owner) + { + std::vector layoutChildren; + + for (const auto child : p_owner.GetChildren()) + { + if (!child || !child->IsActive()) + { + continue; + } + + const auto size = GetLayoutSize(*child); + if (!size) + { + continue; + } + + layoutChildren.push_back({ child, size.value() }); + } + + return layoutChildren; + } + + Clay_LayoutDirection ToClayDirection(OvCore::ECS::Components::UI::CLayoutGroup::EDirection p_direction) + { + return p_direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL ? + CLAY_LEFT_TO_RIGHT : + CLAY_TOP_TO_BOTTOM; + } + + Clay_LayoutAlignmentX ToClayHorizontalAlignment(OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment p_alignment) + { + switch (p_alignment) + { + case OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment::LEFT: + return CLAY_ALIGN_X_LEFT; + case OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment::RIGHT: + return CLAY_ALIGN_X_RIGHT; + case OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment::CENTER: + default: + return CLAY_ALIGN_X_CENTER; + } + } + + Clay_LayoutAlignmentY ToClayVerticalAlignment(OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment p_alignment) + { + switch (p_alignment) + { + case OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment::TOP: + return CLAY_ALIGN_Y_TOP; + case OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment::BOTTOM: + return CLAY_ALIGN_Y_BOTTOM; + case OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment::CENTER: + default: + return CLAY_ALIGN_Y_CENTER; + } + } + + uint16_t ToClaySpacing(float p_spacing) + { + return static_cast(ClampSpacing(p_spacing, kMinimumSpacing)); + } + + Clay_Padding ToClayPadding(const OvMaths::FVector4& p_padding) + { + return { + .left = static_cast(ClampPadding(p_padding.x, kMinimumPadding)), + .right = static_cast(ClampPadding(p_padding.y, kMinimumPadding)), + .top = static_cast(ClampPadding(p_padding.z, kMinimumPadding)), + .bottom = static_cast(ClampPadding(p_padding.w, kMinimumPadding)) + }; + } + + void HandleClayError(Clay_ErrorData) + { + } + + Clay_Context* GetClayContext() + { + static std::vector memory; + static Clay_Context* context = nullptr; + + if (!context) + { + memory.resize(Clay_MinMemorySize()); + const auto arena = Clay_CreateArenaWithCapacityAndMemory(memory.size(), memory.data()); + context = Clay_Initialize(arena, { 1.0f, 1.0f }, { HandleClayError }); + } + + if (context) + { + Clay_SetCurrentContext(context); + } + + return context; + } + + OvMaths::FVector2 GetLayoutSize( + const std::vector& p_children, + OvCore::ECS::Components::UI::CLayoutGroup::EDirection p_direction, + float p_spacing, + const OvMaths::FVector2& p_minimumSize, + Clay_Padding p_padding + ) + { + OvMaths::FVector2 result = OvMaths::FVector2::Zero; + + for (size_t i = 0; i < p_children.size(); ++i) + { + const auto& childSize = p_children[i].size; + + if (p_direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL) + { + result.x += childSize.x; + result.y = std::max(result.y, childSize.y); + } + else + { + result.x = std::max(result.x, childSize.x); + result.y += childSize.y; + } + + if (i > 0) + { + if (p_direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL) + { + result.x += p_spacing; + } + else + { + result.y += p_spacing; + } + } + } + + result.x += static_cast(p_padding.left + p_padding.right); + result.y += static_cast(p_padding.top + p_padding.bottom); + + return { + std::max({ result.x, p_minimumSize.x, 1.0f }), + std::max({ result.y, p_minimumSize.y, 1.0f }) + }; + } + + void ApplyControlledChildrenSizing( + std::vector& p_children, + OvCore::ECS::Components::UI::CLayoutGroup::EDirection p_direction, + bool p_controlChildrenWidth, + bool p_controlChildrenHeight, + const OvMaths::FVector2& p_layoutSize, + uint16_t p_spacing, + Clay_Padding p_padding + ) + { + if (p_children.empty()) + { + return; + } + + if (!p_controlChildrenWidth && !p_controlChildrenHeight) + { + return; + } + + const auto childCount = static_cast(p_children.size()); + const float spacing = static_cast(p_spacing); + const float horizontalPadding = static_cast(p_padding.left + p_padding.right); + const float verticalPadding = static_cast(p_padding.top + p_padding.bottom); + const float availableWidth = std::max(p_layoutSize.x - horizontalPadding, 0.0f); + const float availableHeight = std::max(p_layoutSize.y - verticalPadding, 0.0f); + + float controlledWidth = 0.0f; + if (p_controlChildrenWidth) + { + if (p_direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL) + { + controlledWidth = std::max((availableWidth - std::max(childCount - 1.0f, 0.0f) * spacing) / childCount, 0.0f); + } + else + { + controlledWidth = availableWidth; + } + } + + float controlledHeight = 0.0f; + if (p_controlChildrenHeight) + { + if (p_direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::VERTICAL) + { + controlledHeight = std::max((availableHeight - std::max(childCount - 1.0f, 0.0f) * spacing) / childCount, 0.0f); + } + else + { + controlledHeight = availableHeight; + } + } + + for (auto& child : p_children) + { + if (p_controlChildrenWidth) + { + child.size.x = controlledWidth; + } + + if (p_controlChildrenHeight) + { + child.size.y = controlledHeight; + } + } + } + + void ApplyControlledSizeToChildren( + const std::vector& p_children, + bool p_controlChildrenWidth, + bool p_controlChildrenHeight + ) + { + if (!p_controlChildrenWidth && !p_controlChildrenHeight) + { + return; + } + + for (const auto& child : p_children) + { + if (!child.actor) + { + continue; + } + + if (auto* image = child.actor->GetComponent()) + { + auto nextSize = image->GetSize(); + if (p_controlChildrenWidth) + { + nextSize.x = child.size.x; + } + if (p_controlChildrenHeight) + { + nextSize.y = child.size.y; + } + + if (ShouldUpdateControlledSize(image->GetSize(), nextSize, p_controlChildrenWidth, p_controlChildrenHeight)) + { + image->SetSize(nextSize); + } + } + + if (auto* text = child.actor->GetComponent()) + { + auto nextExtents = text->GetExtents(); + if (p_controlChildrenWidth) + { + nextExtents.x = child.size.x; + } + if (p_controlChildrenHeight) + { + nextExtents.y = child.size.y; + } + + if (ShouldUpdateControlledSize(text->GetExtents(), nextExtents, p_controlChildrenWidth, p_controlChildrenHeight)) + { + text->SetExtents(nextExtents); + } + } + + if (auto* transform2D = child.actor->GetComponent()) + { + auto nextSize = transform2D->GetSize(); + if (p_controlChildrenWidth) + { + nextSize.x = child.size.x; + } + if (p_controlChildrenHeight) + { + nextSize.y = child.size.y; + } + + if (ShouldUpdateControlledSize(transform2D->GetSize(), nextSize, p_controlChildrenWidth, p_controlChildrenHeight)) + { + transform2D->SetSize(nextSize); + } + } + } + } + + void DeclareClayLayout( + const std::vector& p_children, + const OvMaths::FVector2& p_layoutSize, + OvCore::ECS::Components::UI::CLayoutGroup::EDirection p_direction, + uint16_t p_spacing, + Clay_Padding p_padding, + OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment p_horizontalAlignment, + OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment p_verticalAlignment + ) + { + CLAY(CLAY_ID("OverloadUILayoutRoot"), { + .layout = { + .sizing = { CLAY_SIZING_FIXED(p_layoutSize.x), CLAY_SIZING_FIXED(p_layoutSize.y) }, + .padding = p_padding, + .childGap = p_spacing, + .childAlignment = { ToClayHorizontalAlignment(p_horizontalAlignment), ToClayVerticalAlignment(p_verticalAlignment) }, + .layoutDirection = ToClayDirection(p_direction) + } + }) + { + for (int32_t i = 0; i < static_cast(p_children.size()); ++i) + { + const auto& childSize = p_children[i].size; + CLAY(CLAY_IDI("OverloadUILayoutChild", i), { + .layout = { + .sizing = { CLAY_SIZING_FIXED(childSize.x), CLAY_SIZING_FIXED(childSize.y) } + } + }) + { + } + } + } + } + + OvMaths::FVector2 ToChildOffset(const Clay_BoundingBox& p_childBounds, const OvMaths::FVector2& p_layoutSize) + { + return { + p_childBounds.x + p_childBounds.width * 0.5f - p_layoutSize.x * 0.5f, + p_layoutSize.y * 0.5f - (p_childBounds.y + p_childBounds.height * 0.5f) + }; + } +} + +OvCore::ECS::Components::UI::CLayoutGroup::CLayoutGroup(ECS::Actor& p_owner) : +AComponent(p_owner) +{ +} + +std::string OvCore::ECS::Components::UI::CLayoutGroup::GetName() +{ + return "Layout Group"; +} + +std::string OvCore::ECS::Components::UI::CLayoutGroup::GetTypeName() +{ + return std::string{ComponentTraits::Name}; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetDirection(EDirection p_direction) +{ + m_direction = ToDirection(static_cast(p_direction)); +} + +OvCore::ECS::Components::UI::CLayoutGroup::EDirection OvCore::ECS::Components::UI::CLayoutGroup::GetDirection() const +{ + return m_direction; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetSpacing(float p_spacing) +{ + m_spacing = ClampSpacing(p_spacing, m_spacing); +} + +float OvCore::ECS::Components::UI::CLayoutGroup::GetSpacing() const +{ + return m_spacing; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetSize(const OvMaths::FVector2& p_size) +{ + m_size.x = ClampSize(p_size.x, m_size.x); + m_size.y = ClampSize(p_size.y, m_size.y); +} + +const OvMaths::FVector2& OvCore::ECS::Components::UI::CLayoutGroup::GetSize() const +{ + return m_size; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetPadding(const OvMaths::FVector4& p_padding) +{ + m_padding.x = ClampPadding(p_padding.x, m_padding.x); + m_padding.y = ClampPadding(p_padding.y, m_padding.y); + m_padding.z = ClampPadding(p_padding.z, m_padding.z); + m_padding.w = ClampPadding(p_padding.w, m_padding.w); +} + +const OvMaths::FVector4& OvCore::ECS::Components::UI::CLayoutGroup::GetPadding() const +{ + return m_padding; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetHorizontalAlignment(EHorizontalAlignment p_alignment) +{ + m_horizontalAlignment = ToHorizontalAlignment(static_cast(p_alignment)); +} + +OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment OvCore::ECS::Components::UI::CLayoutGroup::GetHorizontalAlignment() const +{ + return m_horizontalAlignment; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetVerticalAlignment(EVerticalAlignment p_alignment) +{ + m_verticalAlignment = ToVerticalAlignment(static_cast(p_alignment)); +} + +OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment OvCore::ECS::Components::UI::CLayoutGroup::GetVerticalAlignment() const +{ + return m_verticalAlignment; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetControlChildrenWidth(bool p_controlChildrenWidth) +{ + m_controlChildrenWidth = p_controlChildrenWidth; +} + +bool OvCore::ECS::Components::UI::CLayoutGroup::GetControlChildrenWidth() const +{ + return m_controlChildrenWidth; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetControlChildrenHeight(bool p_controlChildrenHeight) +{ + m_controlChildrenHeight = p_controlChildrenHeight; +} + +bool OvCore::ECS::Components::UI::CLayoutGroup::GetControlChildrenHeight() const +{ + return m_controlChildrenHeight; +} + +bool OvCore::ECS::Components::UI::CLayoutGroup::IsDirectionEditable() const +{ + return true; +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::CLayoutGroup::GetChildOffset(const ECS::Actor& p_child) const +{ + if (p_child.GetParent() != &owner) + { + return OvMaths::FVector2::Zero; + } + + for (const auto& [child, offset] : GetChildOffsets()) + { + if (child == &p_child) + { + return offset; + } + } + + return OvMaths::FVector2::Zero; +} + +std::vector OvCore::ECS::Components::UI::CLayoutGroup::GetChildOffsets() const +{ + auto layoutChildren = CollectLayoutChildren(owner); + + std::vector offsets; + + if (layoutChildren.empty() || !GetClayContext()) + { + return offsets; + } + + const auto spacing = ToClaySpacing(m_spacing); + const auto padding = ToClayPadding(m_padding); + const auto layoutSize = GetLayoutSize(layoutChildren, m_direction, static_cast(spacing), m_size, padding); + ApplyControlledChildrenSizing( + layoutChildren, + m_direction, + m_controlChildrenWidth, + m_controlChildrenHeight, + layoutSize, + spacing, + padding + ); + + Clay_SetLayoutDimensions({ layoutSize.x, layoutSize.y }); + Clay_BeginLayout(); + DeclareClayLayout(layoutChildren, layoutSize, m_direction, spacing, padding, m_horizontalAlignment, m_verticalAlignment); + Clay_EndLayout(0.0f); + + offsets.reserve(layoutChildren.size()); + + for (int32_t i = 0; i < static_cast(layoutChildren.size()); ++i) + { + const auto childBounds = Clay_GetElementData(CLAY_IDI("OverloadUILayoutChild", i)); + if (childBounds.found) + { + offsets.emplace_back(layoutChildren[i].actor, ToChildOffset(childBounds.boundingBox, layoutSize)); + } + } + + return offsets; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::ApplyControlledChildrenSizes() +{ + if (!m_controlChildrenWidth && !m_controlChildrenHeight) + { + return; + } + + auto layoutChildren = CollectLayoutChildren(owner); + if (layoutChildren.empty()) + { + return; + } + + const auto spacing = ToClaySpacing(m_spacing); + const auto padding = ToClayPadding(m_padding); + const auto layoutSize = GetLayoutSize(layoutChildren, m_direction, static_cast(spacing), m_size, padding); + + ApplyControlledChildrenSizing( + layoutChildren, + m_direction, + m_controlChildrenWidth, + m_controlChildrenHeight, + layoutSize, + spacing, + padding + ); + ApplyControlledSizeToChildren(layoutChildren, m_controlChildrenWidth, m_controlChildrenHeight); +} + +void OvCore::ECS::Components::UI::CLayoutGroup::OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + Helpers::Serializer::SerializeInt(p_doc, p_node, "direction", static_cast(m_direction)); + Helpers::Serializer::SerializeFloat(p_doc, p_node, "spacing", m_spacing); + Helpers::Serializer::SerializeVec2(p_doc, p_node, "size", m_size); + Helpers::Serializer::SerializeVec4(p_doc, p_node, "padding", m_padding); + Helpers::Serializer::SerializeInt(p_doc, p_node, "horizontal_alignment", static_cast(m_horizontalAlignment)); + Helpers::Serializer::SerializeInt(p_doc, p_node, "vertical_alignment", static_cast(m_verticalAlignment)); + Helpers::Serializer::SerializeBoolean(p_doc, p_node, "control_children_width", m_controlChildrenWidth); + Helpers::Serializer::SerializeBoolean(p_doc, p_node, "control_children_height", m_controlChildrenHeight); +} + +void OvCore::ECS::Components::UI::CLayoutGroup::OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + if (p_node->FirstChildElement("direction")) + { + auto direction = static_cast(m_direction); + Helpers::Serializer::DeserializeInt(p_doc, p_node, "direction", direction); + SetDirection(ToDirection(direction)); + } + + if (p_node->FirstChildElement("spacing")) + { + auto spacing = m_spacing; + Helpers::Serializer::DeserializeFloat(p_doc, p_node, "spacing", spacing); + SetSpacing(spacing); + } + + if (p_node->FirstChildElement("size")) + { + auto size = m_size; + Helpers::Serializer::DeserializeVec2(p_doc, p_node, "size", size); + SetSize(size); + } + + if (p_node->FirstChildElement("padding")) + { + auto padding = m_padding; + Helpers::Serializer::DeserializeVec4(p_doc, p_node, "padding", padding); + SetPadding(padding); + } + + if (p_node->FirstChildElement("horizontal_alignment")) + { + auto horizontalAlignment = static_cast(m_horizontalAlignment); + Helpers::Serializer::DeserializeInt(p_doc, p_node, "horizontal_alignment", horizontalAlignment); + SetHorizontalAlignment(ToHorizontalAlignment(horizontalAlignment)); + } + + if (p_node->FirstChildElement("vertical_alignment")) + { + auto verticalAlignment = static_cast(m_verticalAlignment); + Helpers::Serializer::DeserializeInt(p_doc, p_node, "vertical_alignment", verticalAlignment); + SetVerticalAlignment(ToVerticalAlignment(verticalAlignment)); + } + + if (p_node->FirstChildElement("control_children_width")) + { + auto controlChildrenWidth = m_controlChildrenWidth; + Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "control_children_width", controlChildrenWidth); + SetControlChildrenWidth(controlChildrenWidth); + } + + if (p_node->FirstChildElement("control_children_height")) + { + auto controlChildrenHeight = m_controlChildrenHeight; + Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "control_children_height", controlChildrenHeight); + SetControlChildrenHeight(controlChildrenHeight); + } +} + +void OvCore::ECS::Components::UI::CLayoutGroup::OnInspector(OvUI::Internal::WidgetContainer& p_root) +{ + if (IsDirectionEditable()) + { + Helpers::GUIDrawer::CreateTitle(p_root, "Direction"); + auto& direction = p_root.CreateWidget(static_cast(GetDirection())); + direction.choices.emplace(static_cast(EDirection::HORIZONTAL), "Horizontal"); + direction.choices.emplace(static_cast(EDirection::VERTICAL), "Vertical"); + direction.ValueChangedEvent += [this](int p_choice) + { + SetDirection(ToDirection(p_choice)); + }; + } + + Helpers::GUIDrawer::CreateTitle(p_root, "Horizontal Alignment"); + auto& horizontalAlignment = p_root.CreateWidget(static_cast(GetHorizontalAlignment())); + horizontalAlignment.choices.emplace(static_cast(EHorizontalAlignment::LEFT), "Left"); + horizontalAlignment.choices.emplace(static_cast(EHorizontalAlignment::CENTER), "Center"); + horizontalAlignment.choices.emplace(static_cast(EHorizontalAlignment::RIGHT), "Right"); + horizontalAlignment.ValueChangedEvent += [this](int p_choice) + { + SetHorizontalAlignment(ToHorizontalAlignment(p_choice)); + }; + + Helpers::GUIDrawer::CreateTitle(p_root, "Vertical Alignment"); + auto& verticalAlignment = p_root.CreateWidget(static_cast(GetVerticalAlignment())); + verticalAlignment.choices.emplace(static_cast(EVerticalAlignment::TOP), "Top"); + verticalAlignment.choices.emplace(static_cast(EVerticalAlignment::CENTER), "Center"); + verticalAlignment.choices.emplace(static_cast(EVerticalAlignment::BOTTOM), "Bottom"); + verticalAlignment.ValueChangedEvent += [this](int p_choice) + { + SetVerticalAlignment(ToVerticalAlignment(p_choice)); + }; + + Helpers::GUIDrawer::DrawScalar( + p_root, + "Spacing", + std::bind(&CLayoutGroup::GetSpacing, this), + std::bind(&CLayoutGroup::SetSpacing, this, std::placeholders::_1), + 1.0f, + kMinimumSpacing, + kMaximumSpacing + ); + + Helpers::GUIDrawer::DrawVec2( + p_root, + "Size", + [this]() { return GetSize(); }, + [this](OvMaths::FVector2 p_value) { SetSize(p_value); }, + 1.0f, + kMinimumSize + ); + + Helpers::GUIDrawer::DrawVec4( + p_root, + "Padding", + [this]() { return GetPadding(); }, + [this](OvMaths::FVector4 p_value) { SetPadding(p_value); }, + 1.0f, + kMinimumPadding, + kMaximumPadding + ); + + Helpers::GUIDrawer::DrawBoolean( + p_root, + "Control Children Width", + [this]() { return GetControlChildrenWidth(); }, + [this](bool p_value) { SetControlChildrenWidth(p_value); } + ); + + Helpers::GUIDrawer::DrawBoolean( + p_root, + "Control Children Height", + [this]() { return GetControlChildrenHeight(); }, + [this](bool p_value) { SetControlChildrenHeight(p_value); } + ); +} diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/CText.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/CText.cpp new file mode 100644 index 000000000..94b633728 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CText.cpp @@ -0,0 +1,571 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr float kMinimumFontSize = 1.0f; + constexpr float kMinimumExtent = 0.0f; + constexpr const char* kDefaultMaterialPath = ":Materials\\UI_Text.ovmat"; + constexpr const char* kColorUniform = "u_Color"; + + float ClampFinite(float p_value, float p_min) + { + return std::isfinite(p_value) ? std::max(p_value, p_min) : p_min; + } + + float KeepFinite(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? p_value : p_fallback; + } + + OvUI::Types::Color ToColor(const OvMaths::FVector4& p_value) + { + return { p_value.x, p_value.y, p_value.z, p_value.w }; + } + + OvMaths::FVector4 ToVec4(const OvUI::Types::Color& p_value) + { + return { p_value.r, p_value.g, p_value.b, p_value.a }; + } + + OvCore::ECS::Components::UI::CText::EHorizontalAlignment ToHorizontalAlignment(int p_value) + { + using EHorizontalAlignment = OvCore::ECS::Components::UI::CText::EHorizontalAlignment; + + switch (p_value) + { + case static_cast(EHorizontalAlignment::CENTER): + return EHorizontalAlignment::CENTER; + case static_cast(EHorizontalAlignment::RIGHT): + return EHorizontalAlignment::RIGHT; + case static_cast(EHorizontalAlignment::LEFT): + default: + return EHorizontalAlignment::LEFT; + } + } + + OvCore::ECS::Components::UI::CText::EVerticalAlignment ToVerticalAlignment(int p_value) + { + using EVerticalAlignment = OvCore::ECS::Components::UI::CText::EVerticalAlignment; + + switch (p_value) + { + case static_cast(EVerticalAlignment::CENTER): + return EVerticalAlignment::CENTER; + case static_cast(EVerticalAlignment::BOTTOM): + return EVerticalAlignment::BOTTOM; + case static_cast(EVerticalAlignment::TOP): + default: + return EVerticalAlignment::TOP; + } + } + + OvMaths::FVector2 ResolveTextSize(const OvMaths::FVector2& p_contentSize, const OvMaths::FVector2& p_extents) + { + return { + p_extents.x > 0.0f ? p_extents.x : p_contentSize.x, + p_extents.y > 0.0f ? p_extents.y : p_contentSize.y + }; + } + + float GetAlignedCenterX(float p_textWidth, float p_contentWidth, OvCore::ECS::Components::UI::CText::EHorizontalAlignment p_alignment) + { + switch (p_alignment) + { + case OvCore::ECS::Components::UI::CText::EHorizontalAlignment::CENTER: + return 0.0f; + case OvCore::ECS::Components::UI::CText::EHorizontalAlignment::RIGHT: + return p_textWidth * 0.5f - p_contentWidth * 0.5f; + case OvCore::ECS::Components::UI::CText::EHorizontalAlignment::LEFT: + default: + return -p_textWidth * 0.5f + p_contentWidth * 0.5f; + } + } + + float GetAlignedCenterY(float p_textHeight, float p_contentHeight, OvCore::ECS::Components::UI::CText::EVerticalAlignment p_alignment) + { + switch (p_alignment) + { + case OvCore::ECS::Components::UI::CText::EVerticalAlignment::CENTER: + return 0.0f; + case OvCore::ECS::Components::UI::CText::EVerticalAlignment::BOTTOM: + return -p_textHeight * 0.5f + p_contentHeight * 0.5f; + case OvCore::ECS::Components::UI::CText::EVerticalAlignment::TOP: + default: + return p_textHeight * 0.5f - p_contentHeight * 0.5f; + } + } +} + +OvCore::ECS::Components::UI::CText::CText(ECS::Actor& p_owner) : +AComponent(p_owner) +{ +} + +std::string OvCore::ECS::Components::UI::CText::GetName() +{ + return "Text"; +} + +std::string OvCore::ECS::Components::UI::CText::GetTypeName() +{ + return std::string{ComponentTraits::Name}; +} + +void OvCore::ECS::Components::UI::CText::SetText(const std::string& p_text) +{ + m_text = p_text; + MarkMeshDirty(); +} + +const std::string& OvCore::ECS::Components::UI::CText::GetText() const +{ + return m_text; +} + +void OvCore::ECS::Components::UI::CText::SetFontPath(const std::string& p_fontPath) +{ + m_fontPath = p_fontPath; + MarkMeshDirty(); + RefreshMaterial(); +} + +const std::string& OvCore::ECS::Components::UI::CText::GetFontPath() const +{ + return m_fontPath; +} + +void OvCore::ECS::Components::UI::CText::SetFontSize(float p_fontSize) +{ + m_fontSize = ClampFinite(p_fontSize, kMinimumFontSize); + MarkMeshDirty(); +} + +float OvCore::ECS::Components::UI::CText::GetFontSize() const +{ + return m_fontSize; +} + +void OvCore::ECS::Components::UI::CText::SetColor(const OvMaths::FVector4& p_color) +{ + m_color.x = KeepFinite(p_color.x, m_color.x); + m_color.y = KeepFinite(p_color.y, m_color.y); + m_color.z = KeepFinite(p_color.z, m_color.z); + m_color.w = KeepFinite(p_color.w, m_color.w); + RefreshMaterial(); +} + +const OvMaths::FVector4& OvCore::ECS::Components::UI::CText::GetColor() const +{ + return m_color; +} + +void OvCore::ECS::Components::UI::CText::SetExtents(const OvMaths::FVector2& p_extents) +{ + m_extents.x = ClampFinite(p_extents.x, kMinimumExtent); + m_extents.y = ClampFinite(p_extents.y, kMinimumExtent); + MarkMeshDirty(); +} + +const OvMaths::FVector2& OvCore::ECS::Components::UI::CText::GetExtents() const +{ + return m_extents; +} + +void OvCore::ECS::Components::UI::CText::SetHorizontalAlignment(EHorizontalAlignment p_alignment) +{ + m_horizontalAlignment = ToHorizontalAlignment(static_cast(p_alignment)); + MarkMeshDirty(); +} + +OvCore::ECS::Components::UI::CText::EHorizontalAlignment OvCore::ECS::Components::UI::CText::GetHorizontalAlignment() const +{ + return m_horizontalAlignment; +} + +void OvCore::ECS::Components::UI::CText::SetVerticalAlignment(EVerticalAlignment p_alignment) +{ + m_verticalAlignment = ToVerticalAlignment(static_cast(p_alignment)); + MarkMeshDirty(); +} + +OvCore::ECS::Components::UI::CText::EVerticalAlignment OvCore::ECS::Components::UI::CText::GetVerticalAlignment() const +{ + return m_verticalAlignment; +} + +OvRendering::Resources::Mesh* OvCore::ECS::Components::UI::CText::GetMesh() const +{ + RebuildMesh(); + return m_mesh.get(); +} + +OvRendering::Data::Material* OvCore::ECS::Components::UI::CText::GetMaterial() +{ + RefreshMaterial(); + return m_material && m_material->IsValid() ? m_material.get() : nullptr; +} + +const OvMaths::FVector2& OvCore::ECS::Components::UI::CText::GetSize() const +{ + RebuildMesh(); + return m_size; +} + +void OvCore::ECS::Components::UI::CText::OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + Helpers::Serializer::SerializeString(p_doc, p_node, "text", m_text); + Helpers::Serializer::SerializeString(p_doc, p_node, "font_path", m_fontPath); + Helpers::Serializer::SerializeFloat(p_doc, p_node, "font_size", m_fontSize); + Helpers::Serializer::SerializeVec4(p_doc, p_node, "color", m_color); + Helpers::Serializer::SerializeVec2(p_doc, p_node, "extents", m_extents); + Helpers::Serializer::SerializeInt(p_doc, p_node, "horizontal_alignment", static_cast(m_horizontalAlignment)); + Helpers::Serializer::SerializeInt(p_doc, p_node, "vertical_alignment", static_cast(m_verticalAlignment)); +} + +void OvCore::ECS::Components::UI::CText::OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + if (p_node->FirstChildElement("text")) + { + auto text = m_text; + Helpers::Serializer::DeserializeString(p_doc, p_node, "text", text); + SetText(text); + } + + if (p_node->FirstChildElement("font_path")) + { + auto fontPath = m_fontPath; + Helpers::Serializer::DeserializeString(p_doc, p_node, "font_path", fontPath); + SetFontPath(fontPath); + } + + if (p_node->FirstChildElement("font_size")) + { + auto fontSize = m_fontSize; + Helpers::Serializer::DeserializeFloat(p_doc, p_node, "font_size", fontSize); + SetFontSize(fontSize); + } + + if (p_node->FirstChildElement("color")) + { + auto color = m_color; + Helpers::Serializer::DeserializeVec4(p_doc, p_node, "color", color); + SetColor(color); + } + + if (p_node->FirstChildElement("extents")) + { + auto extents = m_extents; + Helpers::Serializer::DeserializeVec2(p_doc, p_node, "extents", extents); + SetExtents(extents); + } + + if (p_node->FirstChildElement("horizontal_alignment")) + { + auto horizontalAlignment = static_cast(m_horizontalAlignment); + Helpers::Serializer::DeserializeInt(p_doc, p_node, "horizontal_alignment", horizontalAlignment); + SetHorizontalAlignment(ToHorizontalAlignment(horizontalAlignment)); + } + + if (p_node->FirstChildElement("vertical_alignment")) + { + auto verticalAlignment = static_cast(m_verticalAlignment); + Helpers::Serializer::DeserializeInt(p_doc, p_node, "vertical_alignment", verticalAlignment); + SetVerticalAlignment(ToVerticalAlignment(verticalAlignment)); + } +} + +void OvCore::ECS::Components::UI::CText::OnInspector(OvUI::Internal::WidgetContainer& p_root) +{ + Helpers::GUIDrawer::CreateTitle(p_root, "Text"); + auto& textInput = p_root.CreateWidget(""); + textInput.multiline = true; + textInput.multilineHeight = 100.0f; + textInput.fullWidth = true; + + auto& textDispatcher = textInput.AddPlugin>(); + textDispatcher.RegisterGatherer([this]() { return GetText(); }); + textDispatcher.RegisterProvider([this](std::string p_value) { SetText(p_value); }); + + Helpers::GUIDrawer::DrawAsset( + p_root, + "Font", + [this]() { return GetFontPath(); }, + [this](std::string p_value) { SetFontPath(p_value); }, + OvTools::Utils::PathParser::EFileType::FONT + ); + + Helpers::GUIDrawer::DrawScalar( + p_root, + "Font Size", + std::bind(&CText::GetFontSize, this), + std::bind(&CText::SetFontSize, this, std::placeholders::_1), + 1.0f, + kMinimumFontSize + ); + + Helpers::GUIDrawer::DrawVec2( + p_root, + "Extents", + [this]() { return GetExtents(); }, + [this](OvMaths::FVector2 p_value) { SetExtents(p_value); }, + 1.0f, + kMinimumExtent + ); + + Helpers::GUIDrawer::CreateTitle(p_root, "Horizontal Alignment"); + auto& horizontalAlignment = p_root.CreateWidget(static_cast(GetHorizontalAlignment())); + horizontalAlignment.choices.emplace(static_cast(EHorizontalAlignment::LEFT), "Left"); + horizontalAlignment.choices.emplace(static_cast(EHorizontalAlignment::CENTER), "Center"); + horizontalAlignment.choices.emplace(static_cast(EHorizontalAlignment::RIGHT), "Right"); + horizontalAlignment.ValueChangedEvent += [this](int p_choice) + { + SetHorizontalAlignment(ToHorizontalAlignment(p_choice)); + }; + + Helpers::GUIDrawer::CreateTitle(p_root, "Vertical Alignment"); + auto& verticalAlignment = p_root.CreateWidget(static_cast(GetVerticalAlignment())); + verticalAlignment.choices.emplace(static_cast(EVerticalAlignment::TOP), "Top"); + verticalAlignment.choices.emplace(static_cast(EVerticalAlignment::CENTER), "Center"); + verticalAlignment.choices.emplace(static_cast(EVerticalAlignment::BOTTOM), "Bottom"); + verticalAlignment.ValueChangedEvent += [this](int p_choice) + { + SetVerticalAlignment(ToVerticalAlignment(p_choice)); + }; + + Helpers::GUIDrawer::DrawColor( + p_root, + "Color", + [this]() { return ToColor(m_color); }, + [this](OvUI::Types::Color p_value) { SetColor(ToVec4(p_value)); }, + true + ); +} + +OvRendering::Resources::Font* OvCore::ECS::Components::UI::CText::GetFont() const +{ + if (m_fontPath.empty()) + { + return nullptr; + } + + return Global::ServiceLocator::Get().GetResource(m_fontPath); +} + +void OvCore::ECS::Components::UI::CText::MarkMeshDirty() +{ + m_meshDirty = true; +} + +void OvCore::ECS::Components::UI::CText::RebuildMesh() const +{ + if (!m_meshDirty) + { + return; + } + + m_meshDirty = false; + m_mesh.reset(); + m_size = ResolveTextSize(OvMaths::FVector2::Zero, m_extents); + + auto* font = GetFont(); + if (!font || m_text.empty() || !font->EnsurePixelSize(m_fontSize)) + { + return; + } + + const float bakedPixelSize = font->GetPixelSize(m_fontSize); + if (bakedPixelSize <= 0.0f) + { + return; + } + + const float scale = m_fontSize / bakedPixelSize; + const float lineHeight = font->GetLineHeight(m_fontSize); + std::vector vertices; + std::vector indices; + vertices.reserve(m_text.size() * 4); + indices.reserve(m_text.size() * 6); + + struct LineInfo + { + size_t firstVertex = 0; + size_t lastVertex = 0; + float minX = std::numeric_limits::max(); + float maxX = std::numeric_limits::lowest(); + bool hasGeometry = false; + }; + + std::vector lines; + lines.push_back({}); + lines.back().firstVertex = 0; + + float cursorX = 0.0f; + float baselineY = 0.0f; + float minX = std::numeric_limits::max(); + float minY = std::numeric_limits::max(); + float maxX = std::numeric_limits::lowest(); + float maxY = std::numeric_limits::lowest(); + + const auto* fallbackGlyph = font->GetGlyph('?', m_fontSize); + + for (const char character : m_text) + { + if (character == '\r') + { + continue; + } + + if (character == '\n') + { + lines.back().lastVertex = vertices.size(); + lines.push_back({}); + lines.back().firstVertex = vertices.size(); + cursorX = 0.0f; + baselineY -= lineHeight * scale; + continue; + } + + const auto* glyph = font->GetGlyph(character, m_fontSize); + if (!glyph) + { + glyph = fallbackGlyph; + } + + if (!glyph) + { + continue; + } + + const float x0 = cursorX + glyph->xOffset * scale; + const float topY = baselineY - glyph->yOffset * scale; + const float x1 = x0 + glyph->width * scale; + const float bottomY = topY - glyph->height * scale; + + const uint32_t firstVertex = static_cast(vertices.size()); + vertices.push_back({ { x0, bottomY, 0.0f }, { glyph->uMin, glyph->vMax }, { 0.0f, 0.0f, 1.0f }, {}, {} }); + vertices.push_back({ { x1, bottomY, 0.0f }, { glyph->uMax, glyph->vMax }, { 0.0f, 0.0f, 1.0f }, {}, {} }); + vertices.push_back({ { x1, topY, 0.0f }, { glyph->uMax, glyph->vMin }, { 0.0f, 0.0f, 1.0f }, {}, {} }); + vertices.push_back({ { x0, topY, 0.0f }, { glyph->uMin, glyph->vMin }, { 0.0f, 0.0f, 1.0f }, {}, {} }); + + indices.push_back(firstVertex + 0); + indices.push_back(firstVertex + 1); + indices.push_back(firstVertex + 2); + indices.push_back(firstVertex + 0); + indices.push_back(firstVertex + 2); + indices.push_back(firstVertex + 3); + + minX = std::min(minX, x0); + minY = std::min(minY, bottomY); + maxX = std::max(maxX, x1); + maxY = std::max(maxY, topY); + + auto& line = lines.back(); + line.hasGeometry = true; + line.minX = std::min(line.minX, x0); + line.maxX = std::max(line.maxX, x1); + line.lastVertex = vertices.size(); + + cursorX += glyph->xAdvance * scale; + } + + lines.back().lastVertex = vertices.size(); + + if (vertices.empty() || indices.empty()) + { + return; + } + + m_size = { + std::max(maxX - minX, 0.0f), + std::max(maxY - minY, 0.0f) + }; + + const auto contentSize = m_size; + m_size = ResolveTextSize(contentSize, m_extents); + + for (const auto& line : lines) + { + if (!line.hasGeometry || line.lastVertex <= line.firstVertex) + { + continue; + } + + const float lineWidth = std::max(line.maxX - line.minX, 0.0f); + const float lineCenterX = line.minX + lineWidth * 0.5f; + const float alignedLineCenterX = GetAlignedCenterX(m_size.x, lineWidth, m_horizontalAlignment); + const float lineOffsetX = alignedLineCenterX - lineCenterX; + + for (size_t vertexIndex = line.firstVertex; vertexIndex < line.lastVertex; ++vertexIndex) + { + vertices[vertexIndex].position[0] += lineOffsetX; + } + } + + const float contentCenterY = minY + contentSize.y * 0.5f; + const float alignedCenterY = GetAlignedCenterY(m_size.y, contentSize.y, m_verticalAlignment); + const float globalOffsetY = alignedCenterY - contentCenterY; + + for (auto& vertex : vertices) + { + vertex.position[1] += globalOffsetY; + } + + m_mesh = std::make_unique(vertices, indices); +} + +void OvCore::ECS::Components::UI::CText::RefreshMaterial() +{ + if (!m_material) + { + m_material = std::make_unique(); + } + + auto* defaultMaterial = Global::ServiceLocator::Get().GetResource(kDefaultMaterialPath); + auto* font = GetFont(); + + if ( + !defaultMaterial || + !defaultMaterial->HasShader() || + !font || + !font->EnsureEmbeddedMaterial(defaultMaterial->GetShader(), m_fontSize) + ) + { + m_material->SetShader(nullptr); + return; + } + + auto* embeddedMaterial = font->GetEmbeddedMaterial(m_fontSize); + if (!embeddedMaterial || !embeddedMaterial->IsValid()) + { + m_material->SetShader(nullptr); + return; + } + + *m_material = *embeddedMaterial; + m_material->TrySetProperty(kColorUniform, m_color); +} diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/CTransform2D.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/CTransform2D.cpp new file mode 100644 index 000000000..533f19c9a --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CTransform2D.cpp @@ -0,0 +1,453 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + constexpr float kDegreesToRadians = 3.14159265359f / 180.0f; + constexpr float kMinimumScale = 0.0001f; + constexpr float kMinimumSize = 0.0f; + constexpr float kMinimumPivot = -1.0f; + constexpr float kMaximumPivot = 1.0f; + + float ClampScaleAxis(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? std::max(p_value, kMinimumScale) : p_fallback; + } + + float KeepFinite(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? p_value : p_fallback; + } + + float ClampPivotAxis(float p_value, float p_fallback) + { + if (!std::isfinite(p_value)) + { + return p_fallback; + } + + return std::clamp(p_value, kMinimumPivot, kMaximumPivot); + } + + OvCore::ECS::Components::UI::CTransform2D::EAnchorPreset ToAnchorPreset(int p_value) + { + using EAnchorPreset = OvCore::ECS::Components::UI::CTransform2D::EAnchorPreset; + + switch (p_value) + { + case static_cast(EAnchorPreset::TOP_LEFT): + return EAnchorPreset::TOP_LEFT; + case static_cast(EAnchorPreset::TOP_CENTER): + return EAnchorPreset::TOP_CENTER; + case static_cast(EAnchorPreset::TOP_RIGHT): + return EAnchorPreset::TOP_RIGHT; + case static_cast(EAnchorPreset::MIDDLE_LEFT): + return EAnchorPreset::MIDDLE_LEFT; + case static_cast(EAnchorPreset::MIDDLE_RIGHT): + return EAnchorPreset::MIDDLE_RIGHT; + case static_cast(EAnchorPreset::BOTTOM_LEFT): + return EAnchorPreset::BOTTOM_LEFT; + case static_cast(EAnchorPreset::BOTTOM_CENTER): + return EAnchorPreset::BOTTOM_CENTER; + case static_cast(EAnchorPreset::BOTTOM_RIGHT): + return EAnchorPreset::BOTTOM_RIGHT; + case static_cast(EAnchorPreset::HORIZONTAL_STRETCH_TOP): + return EAnchorPreset::HORIZONTAL_STRETCH_TOP; + case static_cast(EAnchorPreset::HORIZONTAL_STRETCH_MIDDLE): + return EAnchorPreset::HORIZONTAL_STRETCH_MIDDLE; + case static_cast(EAnchorPreset::HORIZONTAL_STRETCH_BOTTOM): + return EAnchorPreset::HORIZONTAL_STRETCH_BOTTOM; + case static_cast(EAnchorPreset::VERTICAL_STRETCH_LEFT): + return EAnchorPreset::VERTICAL_STRETCH_LEFT; + case static_cast(EAnchorPreset::VERTICAL_STRETCH_CENTER): + return EAnchorPreset::VERTICAL_STRETCH_CENTER; + case static_cast(EAnchorPreset::VERTICAL_STRETCH_RIGHT): + return EAnchorPreset::VERTICAL_STRETCH_RIGHT; + case static_cast(EAnchorPreset::STRETCH_BOTH): + return EAnchorPreset::STRETCH_BOTH; + case static_cast(EAnchorPreset::CENTER): + default: + return EAnchorPreset::CENTER; + } + } + + OvMaths::FVector2 GetAnchorRatio(OvCore::ECS::Components::UI::CTransform2D::EAnchorPreset p_anchorPreset) + { + using EAnchorPreset = OvCore::ECS::Components::UI::CTransform2D::EAnchorPreset; + + switch (p_anchorPreset) + { + case EAnchorPreset::TOP_LEFT: + return { -0.5f, 0.5f }; + case EAnchorPreset::TOP_CENTER: + return { 0.0f, 0.5f }; + case EAnchorPreset::TOP_RIGHT: + return { 0.5f, 0.5f }; + case EAnchorPreset::MIDDLE_LEFT: + return { -0.5f, 0.0f }; + case EAnchorPreset::MIDDLE_RIGHT: + return { 0.5f, 0.0f }; + case EAnchorPreset::BOTTOM_LEFT: + return { -0.5f, -0.5f }; + case EAnchorPreset::BOTTOM_CENTER: + return { 0.0f, -0.5f }; + case EAnchorPreset::BOTTOM_RIGHT: + return { 0.5f, -0.5f }; + case EAnchorPreset::HORIZONTAL_STRETCH_TOP: + return { 0.0f, 0.5f }; + case EAnchorPreset::HORIZONTAL_STRETCH_MIDDLE: + return { 0.0f, 0.0f }; + case EAnchorPreset::HORIZONTAL_STRETCH_BOTTOM: + return { 0.0f, -0.5f }; + case EAnchorPreset::VERTICAL_STRETCH_LEFT: + return { -0.5f, 0.0f }; + case EAnchorPreset::VERTICAL_STRETCH_CENTER: + return { 0.0f, 0.0f }; + case EAnchorPreset::VERTICAL_STRETCH_RIGHT: + return { 0.5f, 0.0f }; + case EAnchorPreset::STRETCH_BOTH: + return { 0.0f, 0.0f }; + case EAnchorPreset::CENTER: + default: + return { 0.0f, 0.0f }; + } + } + + bool IsHorizontalPositionEditable(OvCore::ECS::Components::UI::CTransform2D::EAnchorPreset p_anchorPreset) + { + using EAnchorPreset = OvCore::ECS::Components::UI::CTransform2D::EAnchorPreset; + + switch (p_anchorPreset) + { + case EAnchorPreset::HORIZONTAL_STRETCH_TOP: + case EAnchorPreset::HORIZONTAL_STRETCH_MIDDLE: + case EAnchorPreset::HORIZONTAL_STRETCH_BOTTOM: + case EAnchorPreset::STRETCH_BOTH: + return false; + default: + return true; + } + } + + bool IsVerticalPositionEditable(OvCore::ECS::Components::UI::CTransform2D::EAnchorPreset p_anchorPreset) + { + using EAnchorPreset = OvCore::ECS::Components::UI::CTransform2D::EAnchorPreset; + + switch (p_anchorPreset) + { + case EAnchorPreset::VERTICAL_STRETCH_LEFT: + case EAnchorPreset::VERTICAL_STRETCH_CENTER: + case EAnchorPreset::VERTICAL_STRETCH_RIGHT: + case EAnchorPreset::STRETCH_BOTH: + return false; + default: + return true; + } + } + + bool IsDrivenByLayout(const OvCore::ECS::Actor& p_owner) + { + const auto* parent = p_owner.GetParent(); + return parent && parent->GetComponent(); + } +} + +OvCore::ECS::Components::UI::CTransform2D::CTransform2D(ECS::Actor& p_owner) : +AComponent(p_owner) +{ +} + +std::string OvCore::ECS::Components::UI::CTransform2D::GetName() +{ + return "Transform 2D"; +} + +std::string OvCore::ECS::Components::UI::CTransform2D::GetTypeName() +{ + return std::string{ComponentTraits::Name}; +} + +void OvCore::ECS::Components::UI::CTransform2D::SetPosition(const OvMaths::FVector2& p_position) +{ + m_position.x = KeepFinite(p_position.x, m_position.x); + m_position.y = KeepFinite(p_position.y, m_position.y); +} + +const OvMaths::FVector2& OvCore::ECS::Components::UI::CTransform2D::GetPosition() const +{ + return m_position; +} + +void OvCore::ECS::Components::UI::CTransform2D::SetRotation(float p_rotation) +{ + const auto rotation = KeepFinite(p_rotation, GetRotation()); + auto eulerRotation = OvMaths::FQuaternion::EulerAngles(owner.transform.GetLocalRotation()); + eulerRotation.z = rotation; + owner.transform.SetLocalRotation(OvMaths::FQuaternion(eulerRotation)); +} + +float OvCore::ECS::Components::UI::CTransform2D::GetRotation() const +{ + const auto eulerRotation = OvMaths::FQuaternion::EulerAngles(owner.transform.GetLocalRotation()); + return KeepFinite(eulerRotation.z, 0.0f); +} + +void OvCore::ECS::Components::UI::CTransform2D::SetScale(const OvMaths::FVector2& p_scale) +{ + auto localScale = owner.transform.GetLocalScale(); + localScale.x = ClampScaleAxis(p_scale.x, localScale.x); + localScale.y = ClampScaleAxis(p_scale.y, localScale.y); + owner.transform.SetLocalScale(localScale); +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::CTransform2D::GetScale() const +{ + const auto& localScale = owner.transform.GetLocalScale(); + return { + ClampScaleAxis(localScale.x, kMinimumScale), + ClampScaleAxis(localScale.y, kMinimumScale) + }; +} + +void OvCore::ECS::Components::UI::CTransform2D::SetSize(const OvMaths::FVector2& p_size) +{ + m_size.x = std::isfinite(p_size.x) ? std::max(p_size.x, kMinimumSize) : m_size.x; + m_size.y = std::isfinite(p_size.y) ? std::max(p_size.y, kMinimumSize) : m_size.y; +} + +const OvMaths::FVector2& OvCore::ECS::Components::UI::CTransform2D::GetSize() const +{ + return m_size; +} + +void OvCore::ECS::Components::UI::CTransform2D::SetPivot(const OvMaths::FVector2& p_pivot) +{ + m_pivot.x = ClampPivotAxis(p_pivot.x, m_pivot.x); + m_pivot.y = ClampPivotAxis(p_pivot.y, m_pivot.y); +} + +const OvMaths::FVector2& OvCore::ECS::Components::UI::CTransform2D::GetPivot() const +{ + return m_pivot; +} + +void OvCore::ECS::Components::UI::CTransform2D::SetAnchorPreset(EAnchorPreset p_anchorPreset) +{ + m_anchorPreset = ToAnchorPreset(static_cast(p_anchorPreset)); +} + +OvCore::ECS::Components::UI::CTransform2D::EAnchorPreset OvCore::ECS::Components::UI::CTransform2D::GetAnchorPreset() const +{ + return m_anchorPreset; +} + +OvMaths::FMatrix4 OvCore::ECS::Components::UI::CTransform2D::GetMatrix( + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset, + const OvMaths::FVector2& p_elementSize +) const +{ + const auto position = GetAnchoredPosition(p_canvasSize, p_layoutOffset); + const auto scale = GetScale(); + const auto effectiveSize = GetEffectiveSize(p_elementSize); + const auto halfSize = effectiveSize * 0.5f; + const OvMaths::FVector2 pivotOffset = { + -m_pivot.x * halfSize.x, + m_pivot.y * halfSize.y + }; + + return + OvMaths::FMatrix4::Translation({ position.x, position.y, 0.0f }) * + OvMaths::FMatrix4::RotationOnAxisZ(GetRotation() * kDegreesToRadians) * + OvMaths::FMatrix4::Scaling({ scale.x, scale.y, 1.0f }) * + OvMaths::FMatrix4::Translation({ pivotOffset.x, pivotOffset.y, 0.0f }); +} + +void OvCore::ECS::Components::UI::CTransform2D::OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + Helpers::Serializer::SerializeVec2(p_doc, p_node, "position", m_position); + Helpers::Serializer::SerializeFloat(p_doc, p_node, "rotation", GetRotation()); + Helpers::Serializer::SerializeVec2(p_doc, p_node, "scale", GetScale()); + Helpers::Serializer::SerializeVec2(p_doc, p_node, "size", m_size); + Helpers::Serializer::SerializeVec2(p_doc, p_node, "pivot", m_pivot); + Helpers::Serializer::SerializeInt(p_doc, p_node, "anchor_preset", static_cast(m_anchorPreset)); +} + +void OvCore::ECS::Components::UI::CTransform2D::OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + if (p_node->FirstChildElement("position")) + { + auto position = m_position; + Helpers::Serializer::DeserializeVec2(p_doc, p_node, "position", position); + SetPosition(position); + } + + if (p_node->FirstChildElement("rotation")) + { + auto rotation = GetRotation(); + Helpers::Serializer::DeserializeFloat(p_doc, p_node, "rotation", rotation); + SetRotation(rotation); + } + + if (p_node->FirstChildElement("scale")) + { + auto scale = GetScale(); + Helpers::Serializer::DeserializeVec2(p_doc, p_node, "scale", scale); + SetScale(scale); + } + + if (p_node->FirstChildElement("size")) + { + auto size = m_size; + Helpers::Serializer::DeserializeVec2(p_doc, p_node, "size", size); + SetSize(size); + } + + if (p_node->FirstChildElement("pivot")) + { + auto pivot = m_pivot; + Helpers::Serializer::DeserializeVec2(p_doc, p_node, "pivot", pivot); + SetPivot(pivot); + } + + if (p_node->FirstChildElement("anchor_preset")) + { + auto anchorPreset = static_cast(m_anchorPreset); + Helpers::Serializer::DeserializeInt(p_doc, p_node, "anchor_preset", anchorPreset); + SetAnchorPreset(ToAnchorPreset(anchorPreset)); + } +} + +void OvCore::ECS::Components::UI::CTransform2D::OnInspector(OvUI::Internal::WidgetContainer& p_root) +{ + Helpers::GUIDrawer::CreateTitle(p_root, "Anchored Position (px)"); + auto& anchoredPosition = p_root.CreateWidget>( + Helpers::GUIDrawer::GetDataType(), + Helpers::GUIDrawer::_MIN_FLOAT, + Helpers::GUIDrawer::_MAX_FLOAT, + 0.0f, + 1.0f, + "", + Helpers::GUIDrawer::GetFormat() + ); + auto& anchoredPositionDispatcher = anchoredPosition.AddPlugin>>(); + anchoredPositionDispatcher.RegisterGatherer([this]() + { + const auto value = GetPosition(); + return std::array{ value.x, value.y }; + }); + anchoredPositionDispatcher.RegisterProvider([this](std::array p_value) + { + auto position = GetPosition(); + const auto anchorPreset = GetAnchorPreset(); + + if (IsHorizontalPositionEditable(anchorPreset)) + { + position.x = p_value[0]; + } + + if (IsVerticalPositionEditable(anchorPreset)) + { + position.y = p_value[1]; + } + + SetPosition(position); + }); + + const auto updateAnchoredPositionEditability = [this, anchoredPositionWidget = &anchoredPosition]() + { + const auto anchorPreset = GetAnchorPreset(); + const bool isHorizontalEditable = IsHorizontalPositionEditable(anchorPreset); + const bool isVerticalEditable = IsVerticalPositionEditable(anchorPreset); + + anchoredPositionWidget->disabled = IsDrivenByLayout(owner) || (!isHorizontalEditable && !isVerticalEditable); + }; + updateAnchoredPositionEditability(); + + Helpers::GUIDrawer::DrawVec2( + p_root, + "Size", + [this]() { return GetSize(); }, + [this](OvMaths::FVector2 p_value) { SetSize(p_value); }, + 1.0f, + kMinimumSize + ); + + Helpers::GUIDrawer::DrawVec2( + p_root, + "Pivot", + [this]() { return GetPivot(); }, + [this](OvMaths::FVector2 p_value) { SetPivot(p_value); }, + 0.01f, + kMinimumPivot, + kMaximumPivot + ); + + Helpers::GUIDrawer::CreateTitle(p_root, "Anchor Preset"); + auto& anchorPreset = p_root.CreateWidget(static_cast(GetAnchorPreset())); + anchorPreset.disabled = IsDrivenByLayout(owner); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::TOP_LEFT), "Top Left"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::TOP_CENTER), "Top Center"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::TOP_RIGHT), "Top Right"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::MIDDLE_LEFT), "Middle Left"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::CENTER), "Center"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::MIDDLE_RIGHT), "Middle Right"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::BOTTOM_LEFT), "Bottom Left"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::BOTTOM_CENTER), "Bottom Center"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::BOTTOM_RIGHT), "Bottom Right"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::HORIZONTAL_STRETCH_TOP), "Horizontal Stretch Top"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::HORIZONTAL_STRETCH_MIDDLE), "Horizontal Stretch Middle"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::HORIZONTAL_STRETCH_BOTTOM), "Horizontal Stretch Bottom"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::VERTICAL_STRETCH_LEFT), "Vertical Stretch Left"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::VERTICAL_STRETCH_CENTER), "Vertical Stretch Center"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::VERTICAL_STRETCH_RIGHT), "Vertical Stretch Right"); + anchorPreset.choices.emplace(static_cast(EAnchorPreset::STRETCH_BOTH), "Stretch Both"); + + anchorPreset.ValueChangedEvent += [this, updateAnchoredPositionEditability](int p_choice) + { + SetAnchorPreset(ToAnchorPreset(p_choice)); + updateAnchoredPositionEditability(); + }; +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::CTransform2D::GetAnchoredPosition(const OvMaths::FVector2& p_canvasSize, const OvMaths::FVector2& p_layoutOffset) const +{ + const auto anchorRatio = GetAnchorRatio(m_anchorPreset); + const OvMaths::FVector2 anchorOffset = { + KeepFinite(p_canvasSize.x, 0.0f) * anchorRatio.x, + KeepFinite(p_canvasSize.y, 0.0f) * anchorRatio.y + }; + const float positionX = IsHorizontalPositionEditable(m_anchorPreset) ? m_position.x : 0.0f; + const float positionY = IsVerticalPositionEditable(m_anchorPreset) ? m_position.y : 0.0f; + + return { + anchorOffset.x + p_layoutOffset.x + positionX, + anchorOffset.y + p_layoutOffset.y + positionY + }; +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::CTransform2D::GetEffectiveSize(const OvMaths::FVector2& p_elementSize) const +{ + return { + m_size.x > 0.0f ? m_size.x : std::max(p_elementSize.x, 0.0f), + m_size.y > 0.0f ? m_size.y : std::max(p_elementSize.y, 0.0f) + }; +} diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/CVerticalLayout.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/CVerticalLayout.cpp new file mode 100644 index 000000000..21bb975d1 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CVerticalLayout.cpp @@ -0,0 +1,33 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include + +OvCore::ECS::Components::UI::CVerticalLayout::CVerticalLayout(ECS::Actor& p_owner) : + CLayoutGroup(p_owner) +{ + SetDirection(EDirection::VERTICAL); +} + +std::string OvCore::ECS::Components::UI::CVerticalLayout::GetName() +{ + return "Vertical Layout"; +} + +std::string OvCore::ECS::Components::UI::CVerticalLayout::GetTypeName() +{ + return std::string{ ComponentTraits::Name }; +} + +void OvCore::ECS::Components::UI::CVerticalLayout::SetDirection(EDirection) +{ + CLayoutGroup::SetDirection(EDirection::VERTICAL); +} + +bool OvCore::ECS::Components::UI::CVerticalLayout::IsDirectionEditable() const +{ + return false; +} diff --git a/Sources/OvCore/src/OvCore/Rendering/EngineBufferRenderFeature.cpp b/Sources/OvCore/src/OvCore/Rendering/EngineBufferRenderFeature.cpp index 909bd025e..5e9fe00cc 100644 --- a/Sources/OvCore/src/OvCore/Rendering/EngineBufferRenderFeature.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/EngineBufferRenderFeature.cpp @@ -105,5 +105,24 @@ void OvCore::Rendering::EngineBufferRenderFeature::OnBeforeDraw(OvRendering::Dat .offset = kUBOSize - sizeof(modelMatrix), .size = sizeof(modelMatrix) }); + + if (descriptor->viewMatrixOverride && descriptor->projectionMatrixOverride) + { + struct + { + OvMaths::FMatrix4 viewMatrix; + OvMaths::FMatrix4 projectionMatrix; + OvMaths::FVector3 cameraPosition; + } uboDataPage{ + .viewMatrix = OvMaths::FMatrix4::Transpose(*descriptor->viewMatrixOverride), + .projectionMatrix = OvMaths::FMatrix4::Transpose(*descriptor->projectionMatrixOverride), + .cameraPosition = OvMaths::FVector3::Zero + }; + + m_engineBuffer->Upload(&uboDataPage, OvRendering::HAL::BufferMemoryRange{ + .offset = sizeof(OvMaths::FMatrix4), + .size = sizeof(uboDataPage) + }); + } } } diff --git a/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp b/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp index 08fdbf2df..2e46bcda7 100644 --- a/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp @@ -4,13 +4,21 @@ * @licence: MIT */ +#include +#include #include #include +#include #include #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -23,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -163,6 +172,327 @@ namespace } return probes; } + + OvMaths::FVector2 ClampCanvasSize(const OvMaths::FVector2& p_canvasSize) + { + return OvCore::Rendering::UIRenderingUtils::ClampCanvasSize(p_canvasSize); + } + + OvMaths::FVector2 GetCanvasSize(const OvCore::ECS::Components::UI::CCanvas& p_canvas, const OvMaths::FVector2& p_renderSize) + { + return OvCore::Rendering::UIRenderingUtils::GetCanvasSize(p_canvas, p_renderSize); + } + + float GetCanvasScale(const OvCore::ECS::Components::UI::CCanvas* p_canvas, const OvMaths::FVector2& p_renderSize) + { + return p_canvas ? OvCore::Rendering::UIRenderingUtils::GetCanvasScale(*p_canvas, p_renderSize) : 1.0f; + } + + OvMaths::FMatrix4 CreateUIProjectionMatrix(const OvMaths::FVector2& p_renderSize) + { + const auto renderSize = ClampCanvasSize(p_renderSize); + const auto aspectRatio = renderSize.x / renderSize.y; + + return OvMaths::FMatrix4::CreateOrthographic(renderSize.y * 0.5f, aspectRatio, -1.0f, 1.0f); + } + + float GetUIWorldScale(const OvCore::ECS::Components::UI::CCanvas* p_canvas) + { + return p_canvas ? OvCore::Rendering::UIRenderingUtils::GetUIWorldScale(*p_canvas, false) : 1.0f; + } + + OvMaths::FVector2 GetResolvedElementSize( + const OvCore::ECS::Components::UI::CTransform2D* p_transform2D, + const OvMaths::FVector2& p_elementSize + ) + { + if (!p_transform2D) + { + return { + std::max(p_elementSize.x, 0.0f), + std::max(p_elementSize.y, 0.0f) + }; + } + + const auto sizeOverride = p_transform2D->GetSize(); + return { + sizeOverride.x > 0.0f ? sizeOverride.x : std::max(p_elementSize.x, 0.0f), + sizeOverride.y > 0.0f ? sizeOverride.y : std::max(p_elementSize.y, 0.0f) + }; + } + + EngineDrawableDescriptor CreateUIDrawableDescriptor( + OvCore::ECS::Actor& p_owner, + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset, + const OvMaths::FMatrix4& p_uiProjectionMatrix, + bool p_screenSpace, + float p_worldScale, + float p_canvasScale, + const OvMaths::FVector2& p_elementSize + ) + { + auto* transform2D = p_owner.GetComponent(); + + EngineDrawableDescriptor descriptor{ + .modelMatrix = transform2D ? + transform2D->GetMatrix(p_canvasSize, p_layoutOffset, p_elementSize) : + p_owner.transform.GetFTransform().GetWorldMatrix(), + .userMatrix = OvMaths::FMatrix4::Identity + }; + + if (transform2D && p_elementSize.x > 0.0f && p_elementSize.y > 0.0f) + { + const auto resolvedSize = GetResolvedElementSize(transform2D, p_elementSize); + descriptor.modelMatrix = descriptor.modelMatrix * OvMaths::FMatrix4::Scaling({ + resolvedSize.x / p_elementSize.x, + resolvedSize.y / p_elementSize.y, + 1.0f + }); + } + + const auto unitsScale = p_screenSpace ? p_canvasScale : p_canvasScale * p_worldScale; + descriptor.modelMatrix = + OvMaths::FMatrix4::Scaling({ unitsScale, unitsScale, 1.0f }) * + descriptor.modelMatrix; + + if (p_screenSpace) + { + descriptor.viewMatrixOverride = OvMaths::FMatrix4::Identity; + descriptor.projectionMatrixOverride = p_uiProjectionMatrix; + } + + return descriptor; + } + + OvMaths::FVector2 FindLayoutOffset( + const std::vector& p_offsets, + const OvCore::ECS::Actor& p_child + ) + { + const auto found = std::find_if(p_offsets.begin(), p_offsets.end(), [&p_child](const auto& p_offset) + { + return p_offset.first == &p_child; + }); + + if (found != p_offsets.end()) + { + return found->second; + } + + return OvMaths::FVector2::Zero; + } + + void AppendImageDrawable( + SceneRenderer::SceneDrawablesDescriptor& p_result, + OvCore::ECS::Components::UI::CImage& p_image, + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset, + const OvMaths::FMatrix4& p_uiProjectionMatrix, + bool p_screenSpace, + float p_worldScale, + float p_canvasScale, + int p_drawOrder + ) + { + auto& owner = p_image.owner; + auto* material = p_image.GetMaterial(); + if (!material) return; + + OvRendering::Entities::Drawable drawable{ + .mesh = p_image.GetMesh(), + .material = *material, + .stateMask = material->GenerateStateMask() + }; + + drawable.AddDescriptor({ + .actor = owner, + .visibilityFlags = EVisibilityFlags::GEOMETRY, + .bounds = std::nullopt, + .drawOrderOverride = p_drawOrder, + .isUserInterface = true + }); + + drawable.AddDescriptor( + CreateUIDrawableDescriptor( + owner, + p_canvasSize, + p_layoutOffset, + p_uiProjectionMatrix, + p_screenSpace, + p_worldScale, + p_canvasScale, + p_image.GetSize() + ) + ); + + p_result.drawables.push_back(drawable); + } + + void AppendTextDrawable( + SceneRenderer::SceneDrawablesDescriptor& p_result, + OvCore::ECS::Components::UI::CText& p_text, + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset, + const OvMaths::FMatrix4& p_uiProjectionMatrix, + bool p_screenSpace, + float p_worldScale, + float p_canvasScale, + int p_drawOrder + ) + { + auto& owner = p_text.owner; + auto* material = p_text.GetMaterial(); + if (!material) return; + + auto* mesh = p_text.GetMesh(); + if (!mesh) return; + + OvRendering::Entities::Drawable drawable{ + .mesh = *mesh, + .material = *material, + .stateMask = material->GenerateStateMask() + }; + + drawable.AddDescriptor({ + .actor = owner, + .visibilityFlags = EVisibilityFlags::GEOMETRY, + .bounds = std::nullopt, + .drawOrderOverride = p_drawOrder, + .isUserInterface = true + }); + + drawable.AddDescriptor( + CreateUIDrawableDescriptor( + owner, + p_canvasSize, + p_layoutOffset, + p_uiProjectionMatrix, + p_screenSpace, + p_worldScale, + p_canvasScale, + p_text.GetSize() + ) + ); + + p_result.drawables.push_back(drawable); + } + + void AppendHierarchyUIDrawables( + SceneRenderer::SceneDrawablesDescriptor& p_result, + OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_renderSize, + const OvCore::ECS::Components::UI::CCanvas* p_canvas, + OvMaths::FVector2 p_canvasSize, + OvMaths::FVector2 p_layoutOffset, + const OvMaths::FMatrix4& p_uiProjectionMatrix, + bool p_screenSpace, + int& p_drawOrder + ) + { + if (!p_actor.IsActive()) + { + return; + } + + if (auto* canvas = p_actor.GetComponent()) + { + p_canvas = canvas; + p_canvasSize = GetCanvasSize(*canvas, p_renderSize); + } + + if (p_canvas) + { + const auto worldScale = GetUIWorldScale(p_canvas); + const auto canvasScale = GetCanvasScale(p_canvas, p_renderSize); + + if (auto* image = p_actor.GetComponent()) + { + AppendImageDrawable( + p_result, + *image, + p_canvasSize, + p_layoutOffset, + p_uiProjectionMatrix, + p_screenSpace, + worldScale, + canvasScale, + p_drawOrder++ + ); + } + + if (auto* text = p_actor.GetComponent()) + { + AppendTextDrawable( + p_result, + *text, + p_canvasSize, + p_layoutOffset, + p_uiProjectionMatrix, + p_screenSpace, + worldScale, + canvasScale, + p_drawOrder++ + ); + } + } + + std::vector childOffsets; + if (auto* layout = p_actor.GetComponent()) + { + layout->ApplyControlledChildrenSizes(); + childOffsets = layout->GetChildOffsets(); + } + + for (auto* child : p_actor.GetChildren()) + { + if (child) + { + const auto childLayoutOffset = p_layoutOffset + FindLayoutOffset(childOffsets, *child); + + AppendHierarchyUIDrawables( + p_result, + *child, + p_renderSize, + p_canvas, + p_canvasSize, + childLayoutOffset, + p_uiProjectionMatrix, + p_screenSpace, + p_drawOrder + ); + } + } + } + + void AppendHierarchyUIDrawables( + SceneRenderer::SceneDrawablesDescriptor& p_result, + OvCore::SceneSystem::Scene& p_scene, + const OvMaths::FVector2& p_renderSize, + bool p_screenSpace + ) + { + int drawOrder = 0; + const auto uiProjectionMatrix = CreateUIProjectionMatrix(p_renderSize); + + for (auto* actor : p_scene.GetActors()) + { + if (actor && !actor->HasParent()) + { + AppendHierarchyUIDrawables( + p_result, + *actor, + p_renderSize, + nullptr, + OvMaths::FVector2::Zero, + OvMaths::FVector2::Zero, + uiProjectionMatrix, + p_screenSpace, + drawOrder + ); + } + } + } } OvCore::Rendering::SceneRenderer::SceneRenderer(OvRendering::Context::Driver& p_driver, bool p_stencilWrite) @@ -216,12 +546,17 @@ void OvCore::Rendering::SceneRenderer::BeginFrame(const OvRendering::Data::Frame AddDescriptor({ ParseScene(SceneParsingInput{ - .scene = sceneDescriptor.scene + .scene = sceneDescriptor.scene, + .renderSize = { + static_cast(p_frameDescriptor.renderWidth), + static_cast(p_frameDescriptor.renderHeight) + }, + .renderUIInScreenSpace = sceneDescriptor.renderUIInScreenSpace }) }); // Default filtered drawables descriptor using the main camera (used by most render passes). - // Some other render passes can decide to filter the drawables themselves, using the + // Some other render passes can decide to filter the drawables themselves, using the // SceneDrawablesDescriptor instead of the SceneFilteredDrawablesDescriptor one. AddDescriptor({ FilterDrawables( @@ -231,7 +566,8 @@ void OvCore::Rendering::SceneRenderer::BeginFrame(const OvRendering::Data::Frame .frustumOverride = sceneDescriptor.frustumOverride, .overrideMaterial = sceneDescriptor.overrideMaterial, .fallbackMaterial = sceneDescriptor.fallbackMaterial, - .requiredVisibilityFlags = EVisibilityFlags::GEOMETRY + .requiredVisibilityFlags = EVisibilityFlags::GEOMETRY, + .includeUI = sceneDescriptor.includeUI } ) }); @@ -268,7 +604,7 @@ SceneRenderer::SceneDrawablesDescriptor OvCore::Rendering::SceneRenderer::ParseS // Containers for the parsed drawables. SceneRenderer::SceneDrawablesDescriptor result; - const auto& scene = p_input.scene; + auto& scene = p_input.scene; for (const auto modelRenderer : scene.GetFastAccessComponents().modelRenderers) { @@ -316,7 +652,7 @@ SceneRenderer::SceneDrawablesDescriptor OvCore::Rendering::SceneRenderer::ParseS .visibilityFlags = materialRenderer->GetVisibilityFlags(), .bounds = bounds }); - + drawable.AddDescriptor({ transform.GetWorldMatrix(), materialRenderer->GetUserMatrix() @@ -331,6 +667,8 @@ SceneRenderer::SceneDrawablesDescriptor OvCore::Rendering::SceneRenderer::ParseS } } + AppendHierarchyUIDrawables(result, scene, p_input.renderSize, p_input.renderUIInScreenSpace); + return result; } @@ -368,7 +706,12 @@ SceneRenderer::SceneFilteredDrawablesDescriptor OvCore::Rendering::SceneRenderer continue; } - const auto targetMaterial = + if (desc.isUserInterface && !p_filteringInput.includeUI) + { + continue; + } + + const auto targetMaterial = p_filteringInput.overrideMaterial.has_value() ? p_filteringInput.overrideMaterial.value() : (drawable.material.has_value() ? drawable.material.value() : p_filteringInput.fallbackMaterial); @@ -429,30 +772,32 @@ SceneRenderer::SceneFilteredDrawablesDescriptor OvCore::Rendering::SceneRenderer drawableCopy.featureSetOverride = std::nullopt; } + const auto drawOrder = desc.drawOrderOverride.value_or(drawableCopy.material->GetDrawOrder()); + // Categorize drawable based on their type. // This is also where sorting happens, using // the multimap key. if (drawableCopy.material->IsUserInterface()) { output.ui.emplace(decltype(decltype(output.ui)::value_type::first){ - .order = drawableCopy.material->GetDrawOrder(), - .materialKey = reinterpret_cast(&drawableCopy.material.value()), + .order = drawOrder, + .materialKey = &drawableCopy.material.value(), .distance = distanceToCamera }, drawableCopy); } else if (drawableCopy.material->IsBlendable()) { output.transparents.emplace(decltype(decltype(output.transparents)::value_type::first){ - .order = drawableCopy.material->GetDrawOrder(), - .materialKey = reinterpret_cast(&drawableCopy.material.value()), + .order = drawOrder, + .materialKey = &drawableCopy.material.value(), .distance = distanceToCamera }, drawableCopy); } else { output.opaques.emplace(decltype(decltype(output.opaques)::value_type::first){ - .order = drawableCopy.material->GetDrawOrder(), - .materialKey = reinterpret_cast(&drawableCopy.material.value()), + .order = drawOrder, + .materialKey = &drawableCopy.material.value(), .distance = distanceToCamera }, drawableCopy); } diff --git a/Sources/OvCore/src/OvCore/Rendering/UIRenderingUtils.cpp b/Sources/OvCore/src/OvCore/Rendering/UIRenderingUtils.cpp new file mode 100644 index 000000000..c9d3631e9 --- /dev/null +++ b/Sources/OvCore/src/OvCore/Rendering/UIRenderingUtils.cpp @@ -0,0 +1,147 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include + +#include +#include +#include +#include + +namespace +{ + constexpr float kMinimumCanvasScale = 0.0001f; + + float ClampFinite(float p_value, float p_min) + { + return std::isfinite(p_value) ? std::max(p_value, p_min) : p_min; + } +} + +OvMaths::FVector2 OvCore::Rendering::UIRenderingUtils::ClampCanvasSize(const OvMaths::FVector2& p_canvasSize) +{ + return { + std::max(p_canvasSize.x, 1.0f), + std::max(p_canvasSize.y, 1.0f) + }; +} + +float OvCore::Rendering::UIRenderingUtils::GetCanvasScale( + const OvCore::ECS::Components::UI::CCanvas& p_canvas, + const OvMaths::FVector2& p_renderSize +) +{ + const auto renderSize = ClampCanvasSize(p_renderSize); + const auto referenceResolution = ClampCanvasSize(p_canvas.GetReferenceResolution()); + const auto scaleFactor = ClampFinite(p_canvas.GetScaleFactor(), kMinimumCanvasScale); + + if (p_canvas.GetScalerMode() == OvCore::ECS::Components::UI::CCanvas::EScalerMode::CONSTANT_PIXEL_SIZE) + { + return scaleFactor; + } + + const float widthScale = renderSize.x / referenceResolution.x; + const float heightScale = renderSize.y / referenceResolution.y; + float screenScale = 1.0f; + + switch (p_canvas.GetScreenMatchMode()) + { + case OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode::EXPAND: + screenScale = std::min(widthScale, heightScale); + break; + case OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode::SHRINK: + screenScale = std::max(widthScale, heightScale); + break; + case OvCore::ECS::Components::UI::CCanvas::EScreenMatchMode::MATCH_WIDTH_OR_HEIGHT: + default: + { + const auto match = std::clamp(p_canvas.GetMatchWidthOrHeight(), 0.0f, 1.0f); + const auto logWidth = std::log2(std::max(widthScale, kMinimumCanvasScale)); + const auto logHeight = std::log2(std::max(heightScale, kMinimumCanvasScale)); + screenScale = std::pow(2.0f, logWidth + (logHeight - logWidth) * match); + break; + } + } + + return ClampFinite(screenScale * scaleFactor, kMinimumCanvasScale); +} + +OvMaths::FVector2 OvCore::Rendering::UIRenderingUtils::GetCanvasSize( + const OvCore::ECS::Components::UI::CCanvas& p_canvas, + const OvMaths::FVector2& p_renderSize +) +{ + if (p_canvas.GetScalerMode() == OvCore::ECS::Components::UI::CCanvas::EScalerMode::CONSTANT_PIXEL_SIZE) + { + return ClampCanvasSize(p_canvas.GetReferenceResolution()); + } + + const auto renderSize = ClampCanvasSize(p_renderSize); + const auto canvasScale = GetCanvasScale(p_canvas, renderSize); + return ClampCanvasSize(renderSize / canvasScale); +} + +const OvCore::ECS::Components::UI::CCanvas* OvCore::Rendering::UIRenderingUtils::FindCanvas(const OvCore::ECS::Actor& p_owner) +{ + const auto* current = &p_owner; + + while (current) + { + if (const auto* canvas = current->GetComponent(); canvas) + { + return canvas; + } + + current = current->GetParent(); + } + + return nullptr; +} + +OvMaths::FVector2 OvCore::Rendering::UIRenderingUtils::GetCanvasSize( + const OvCore::ECS::Actor& p_owner, + const OvMaths::FVector2& p_renderSize +) +{ + if (const auto* canvas = FindCanvas(p_owner)) + { + return GetCanvasSize(*canvas, p_renderSize); + } + + return ClampCanvasSize(p_renderSize); +} + +OvMaths::FVector2 OvCore::Rendering::UIRenderingUtils::GetLayoutOffset(const OvCore::ECS::Actor& p_owner) +{ + OvMaths::FVector2 result = OvMaths::FVector2::Zero; + const auto* child = &p_owner; + + while (const auto* parent = child->GetParent()) + { + if (const auto* layout = parent->GetComponent()) + { + result += layout->GetChildOffset(*child); + } + + child = parent; + } + + return result; +} + +float OvCore::Rendering::UIRenderingUtils::GetUIWorldScale( + const OvCore::ECS::Components::UI::CCanvas& p_canvas, + bool p_screenSpace +) +{ + if (p_screenSpace) + { + return 1.0f; + } + + return 1.0f / p_canvas.GetPixelsPerUnit(); +} diff --git a/Sources/OvCore/src/OvCore/ResourceManagement/FontManager.cpp b/Sources/OvCore/src/OvCore/ResourceManagement/FontManager.cpp new file mode 100644 index 000000000..6f9aca5c6 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ResourceManagement/FontManager.cpp @@ -0,0 +1,29 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include + +OvRendering::Resources::Font* OvCore::ResourceManagement::FontManager::CreateResource(const std::filesystem::path& p_path) +{ + auto* font = new OvRendering::Resources::Font(p_path.string(), GetRealPath(p_path)); + if (!font->IsValid()) + { + delete font; + return nullptr; + } + + return font; +} + +void OvCore::ResourceManagement::FontManager::DestroyResource(OvRendering::Resources::Font* p_resource) +{ + delete p_resource; +} + +void OvCore::ResourceManagement::FontManager::ReloadResource(OvRendering::Resources::Font* p_resource, const std::filesystem::path& p_path) +{ + p_resource->Reload(GetRealPath(p_path)); +} diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp index bc3ec5e11..6f6159579 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp @@ -26,6 +26,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #include void BindLuaActor(sol::state& p_luaState) @@ -72,6 +79,13 @@ void BindLuaActor(sol::state& p_luaState) "GetAudioListener", &Actor::GetComponent, "GetPostProcessStack", & Actor::GetComponent, "GetReflectionProbe", &Actor::GetComponent, + "GetCanvas", &Actor::GetComponent, + "GetHorizontalLayout", &Actor::GetComponent, + "GetImage", &Actor::GetComponent, + "GetLayoutGroup", &Actor::GetComponent, + "GetText", &Actor::GetComponent, + "GetTransform2D", &Actor::GetComponent, + "GetVerticalLayout", &Actor::GetComponent, /* Behaviours relatives */ "GetBehaviour", [](Actor& p_this, const std::string& p_name) -> sol::table { @@ -125,6 +139,13 @@ void BindLuaActor(sol::state& p_luaState) "AddAudioListener", &Actor::AddComponent, "AddPostProcessStack", & Actor::AddComponent, "AddReflectionProbe", &Actor::AddComponent, + "AddCanvas", &Actor::AddComponent, + "AddHorizontalLayout", &Actor::AddComponent, + "AddImage", &Actor::AddComponent, + "AddLayoutGroup", &Actor::AddComponent, + "AddText", &Actor::AddComponent, + "AddTransform2D", &Actor::AddComponent, + "AddVerticalLayout", &Actor::AddComponent, /* Components Destructors */ "RemoveModelRenderer", &Actor::RemoveComponent, @@ -143,6 +164,13 @@ void BindLuaActor(sol::state& p_luaState) "RemoveAudioListener", &Actor::RemoveComponent, "RemovePostProcessStack", & Actor::RemoveComponent, "RemoveReflectionProbe", &Actor::RemoveComponent, + "RemoveCanvas", &Actor::RemoveComponent, + "RemoveHorizontalLayout", &Actor::RemoveComponent, + "RemoveImage", &Actor::RemoveComponent, + "RemoveLayoutGroup", &Actor::RemoveComponent, + "RemoveText", &Actor::RemoveComponent, + "RemoveTransform2D", &Actor::RemoveComponent, + "RemoveVerticalLayout", &Actor::RemoveComponent, /* Behaviour management */ "AddBehaviour", &Actor::AddBehaviour, diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp index 816842ff6..207e891f7 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp @@ -21,9 +21,16 @@ #include #include #include -#include -#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include void BindLuaComponents(sol::state& p_luaState) { @@ -203,6 +210,181 @@ void BindLuaComponents(sol::state& p_luaState) "SetProjectionMode", &CCamera::SetProjectionMode ); + p_luaState.new_enum("CanvasScalerMode", { + {"CONSTANT_PIXEL_SIZE", UI::CCanvas::EScalerMode::CONSTANT_PIXEL_SIZE}, + {"SCALE_WITH_SCREEN_SIZE", UI::CCanvas::EScalerMode::SCALE_WITH_SCREEN_SIZE} + }); + + p_luaState.new_enum("CanvasScreenMatchMode", { + {"MATCH_WIDTH_OR_HEIGHT", UI::CCanvas::EScreenMatchMode::MATCH_WIDTH_OR_HEIGHT}, + {"EXPAND", UI::CCanvas::EScreenMatchMode::EXPAND}, + {"SHRINK", UI::CCanvas::EScreenMatchMode::SHRINK} + }); + + p_luaState.new_enum("AnchorPreset", { + {"TOP_LEFT", UI::CTransform2D::EAnchorPreset::TOP_LEFT}, + {"TOP_CENTER", UI::CTransform2D::EAnchorPreset::TOP_CENTER}, + {"TOP_RIGHT", UI::CTransform2D::EAnchorPreset::TOP_RIGHT}, + {"MIDDLE_LEFT", UI::CTransform2D::EAnchorPreset::MIDDLE_LEFT}, + {"CENTER", UI::CTransform2D::EAnchorPreset::CENTER}, + {"MIDDLE_RIGHT", UI::CTransform2D::EAnchorPreset::MIDDLE_RIGHT}, + {"BOTTOM_LEFT", UI::CTransform2D::EAnchorPreset::BOTTOM_LEFT}, + {"BOTTOM_CENTER", UI::CTransform2D::EAnchorPreset::BOTTOM_CENTER}, + {"BOTTOM_RIGHT", UI::CTransform2D::EAnchorPreset::BOTTOM_RIGHT}, + {"HORIZONTAL_STRETCH_TOP", UI::CTransform2D::EAnchorPreset::HORIZONTAL_STRETCH_TOP}, + {"HORIZONTAL_STRETCH_MIDDLE", UI::CTransform2D::EAnchorPreset::HORIZONTAL_STRETCH_MIDDLE}, + {"HORIZONTAL_STRETCH_BOTTOM", UI::CTransform2D::EAnchorPreset::HORIZONTAL_STRETCH_BOTTOM}, + {"VERTICAL_STRETCH_LEFT", UI::CTransform2D::EAnchorPreset::VERTICAL_STRETCH_LEFT}, + {"VERTICAL_STRETCH_CENTER", UI::CTransform2D::EAnchorPreset::VERTICAL_STRETCH_CENTER}, + {"VERTICAL_STRETCH_RIGHT", UI::CTransform2D::EAnchorPreset::VERTICAL_STRETCH_RIGHT}, + {"STRETCH_BOTH", UI::CTransform2D::EAnchorPreset::STRETCH_BOTH} + }); + + p_luaState.new_enum("LayoutDirection", { + {"HORIZONTAL", UI::CLayoutGroup::EDirection::HORIZONTAL}, + {"VERTICAL", UI::CLayoutGroup::EDirection::VERTICAL} + }); + + p_luaState.new_enum("LayoutHorizontalAlignment", { + {"LEFT", UI::CLayoutGroup::EHorizontalAlignment::LEFT}, + {"CENTER", UI::CLayoutGroup::EHorizontalAlignment::CENTER}, + {"RIGHT", UI::CLayoutGroup::EHorizontalAlignment::RIGHT} + }); + + p_luaState.new_enum("LayoutVerticalAlignment", { + {"TOP", UI::CLayoutGroup::EVerticalAlignment::TOP}, + {"CENTER", UI::CLayoutGroup::EVerticalAlignment::CENTER}, + {"BOTTOM", UI::CLayoutGroup::EVerticalAlignment::BOTTOM} + }); + + p_luaState.new_enum("TextHorizontalAlignment", { + {"LEFT", UI::CText::EHorizontalAlignment::LEFT}, + {"CENTER", UI::CText::EHorizontalAlignment::CENTER}, + {"RIGHT", UI::CText::EHorizontalAlignment::RIGHT} + }); + + p_luaState.new_enum("TextVerticalAlignment", { + {"TOP", UI::CText::EVerticalAlignment::TOP}, + {"CENTER", UI::CText::EVerticalAlignment::CENTER}, + {"BOTTOM", UI::CText::EVerticalAlignment::BOTTOM} + }); + + p_luaState.new_usertype("Canvas", + sol::base_classes, sol::bases(), + "GetReferenceResolution", [](UI::CCanvas& p_this) -> FVector2 { return p_this.GetReferenceResolution(); }, + "SetReferenceResolution", &UI::CCanvas::SetReferenceResolution, + "GetScaleFactor", &UI::CCanvas::GetScaleFactor, + "SetScaleFactor", &UI::CCanvas::SetScaleFactor, + "GetPixelsPerUnit", &UI::CCanvas::GetPixelsPerUnit, + "SetPixelsPerUnit", &UI::CCanvas::SetPixelsPerUnit, + "GetScalerMode", &UI::CCanvas::GetScalerMode, + "SetScalerMode", &UI::CCanvas::SetScalerMode, + "GetScreenMatchMode", &UI::CCanvas::GetScreenMatchMode, + "SetScreenMatchMode", &UI::CCanvas::SetScreenMatchMode, + "GetMatchWidthOrHeight", &UI::CCanvas::GetMatchWidthOrHeight, + "SetMatchWidthOrHeight", &UI::CCanvas::SetMatchWidthOrHeight + ); + + p_luaState.new_usertype("Image", + sol::base_classes, sol::bases(), + "GetTexture", &UI::CImage::GetTexture, + "SetTexture", &UI::CImage::SetTexture, + "GetSize", [](UI::CImage& p_this) -> FVector2 { return p_this.GetSize(); }, + "SetSize", &UI::CImage::SetSize, + "GetTint", [](UI::CImage& p_this) -> FVector4 { return p_this.GetTint(); }, + "SetTint", &UI::CImage::SetTint + ); + + p_luaState.new_usertype("LayoutGroup", + sol::base_classes, sol::bases(), + "GetDirection", &UI::CLayoutGroup::GetDirection, + "SetDirection", &UI::CLayoutGroup::SetDirection, + "GetSpacing", &UI::CLayoutGroup::GetSpacing, + "SetSpacing", &UI::CLayoutGroup::SetSpacing, + "GetSize", [](UI::CLayoutGroup& p_this) -> FVector2 { return p_this.GetSize(); }, + "SetSize", &UI::CLayoutGroup::SetSize, + "GetPadding", [](UI::CLayoutGroup& p_this) -> FVector4 { return p_this.GetPadding(); }, + "SetPadding", &UI::CLayoutGroup::SetPadding, + "GetHorizontalAlignment", &UI::CLayoutGroup::GetHorizontalAlignment, + "SetHorizontalAlignment", &UI::CLayoutGroup::SetHorizontalAlignment, + "GetVerticalAlignment", &UI::CLayoutGroup::GetVerticalAlignment, + "SetVerticalAlignment", &UI::CLayoutGroup::SetVerticalAlignment, + "GetControlChildrenWidth", &UI::CLayoutGroup::GetControlChildrenWidth, + "SetControlChildrenWidth", &UI::CLayoutGroup::SetControlChildrenWidth, + "GetControlChildrenHeight", &UI::CLayoutGroup::GetControlChildrenHeight, + "SetControlChildrenHeight", &UI::CLayoutGroup::SetControlChildrenHeight + ); + + p_luaState.new_usertype("HorizontalLayout", + sol::base_classes, sol::bases(), + "GetSpacing", &UI::CHorizontalLayout::GetSpacing, + "SetSpacing", &UI::CHorizontalLayout::SetSpacing, + "GetSize", [](UI::CHorizontalLayout& p_this) -> FVector2 { return p_this.GetSize(); }, + "SetSize", &UI::CHorizontalLayout::SetSize, + "GetPadding", [](UI::CHorizontalLayout& p_this) -> FVector4 { return p_this.GetPadding(); }, + "SetPadding", &UI::CHorizontalLayout::SetPadding, + "GetHorizontalAlignment", &UI::CHorizontalLayout::GetHorizontalAlignment, + "SetHorizontalAlignment", &UI::CHorizontalLayout::SetHorizontalAlignment, + "GetVerticalAlignment", &UI::CHorizontalLayout::GetVerticalAlignment, + "SetVerticalAlignment", &UI::CHorizontalLayout::SetVerticalAlignment, + "GetControlChildrenWidth", &UI::CHorizontalLayout::GetControlChildrenWidth, + "SetControlChildrenWidth", &UI::CHorizontalLayout::SetControlChildrenWidth, + "GetControlChildrenHeight", &UI::CHorizontalLayout::GetControlChildrenHeight, + "SetControlChildrenHeight", &UI::CHorizontalLayout::SetControlChildrenHeight + ); + + p_luaState.new_usertype("VerticalLayout", + sol::base_classes, sol::bases(), + "GetSpacing", &UI::CVerticalLayout::GetSpacing, + "SetSpacing", &UI::CVerticalLayout::SetSpacing, + "GetSize", [](UI::CVerticalLayout& p_this) -> FVector2 { return p_this.GetSize(); }, + "SetSize", &UI::CVerticalLayout::SetSize, + "GetPadding", [](UI::CVerticalLayout& p_this) -> FVector4 { return p_this.GetPadding(); }, + "SetPadding", &UI::CVerticalLayout::SetPadding, + "GetHorizontalAlignment", &UI::CVerticalLayout::GetHorizontalAlignment, + "SetHorizontalAlignment", &UI::CVerticalLayout::SetHorizontalAlignment, + "GetVerticalAlignment", &UI::CVerticalLayout::GetVerticalAlignment, + "SetVerticalAlignment", &UI::CVerticalLayout::SetVerticalAlignment, + "GetControlChildrenWidth", &UI::CVerticalLayout::GetControlChildrenWidth, + "SetControlChildrenWidth", &UI::CVerticalLayout::SetControlChildrenWidth, + "GetControlChildrenHeight", &UI::CVerticalLayout::GetControlChildrenHeight, + "SetControlChildrenHeight", &UI::CVerticalLayout::SetControlChildrenHeight + ); + + p_luaState.new_usertype("Text", + sol::base_classes, sol::bases(), + "GetText", &UI::CText::GetText, + "SetText", &UI::CText::SetText, + "GetFontPath", &UI::CText::GetFontPath, + "SetFontPath", &UI::CText::SetFontPath, + "GetFontSize", &UI::CText::GetFontSize, + "SetFontSize", &UI::CText::SetFontSize, + "GetColor", [](UI::CText& p_this) -> FVector4 { return p_this.GetColor(); }, + "SetColor", &UI::CText::SetColor, + "GetExtents", [](UI::CText& p_this) -> FVector2 { return p_this.GetExtents(); }, + "SetExtents", &UI::CText::SetExtents, + "GetHorizontalAlignment", &UI::CText::GetHorizontalAlignment, + "SetHorizontalAlignment", &UI::CText::SetHorizontalAlignment, + "GetVerticalAlignment", &UI::CText::GetVerticalAlignment, + "SetVerticalAlignment", &UI::CText::SetVerticalAlignment + ); + + p_luaState.new_usertype("Transform2D", + sol::base_classes, sol::bases(), + "GetPosition", [](UI::CTransform2D& p_this) -> FVector2 { return p_this.GetPosition(); }, + "SetPosition", &UI::CTransform2D::SetPosition, + "GetRotation", &UI::CTransform2D::GetRotation, + "SetRotation", &UI::CTransform2D::SetRotation, + "GetScale", [](UI::CTransform2D& p_this) -> FVector2 { return p_this.GetScale(); }, + "SetScale", &UI::CTransform2D::SetScale, + "GetSize", [](UI::CTransform2D& p_this) -> FVector2 { return p_this.GetSize(); }, + "SetSize", &UI::CTransform2D::SetSize, + "GetPivot", [](UI::CTransform2D& p_this) -> FVector2 { return p_this.GetPivot(); }, + "SetPivot", &UI::CTransform2D::SetPivot, + "GetAnchorPreset", &UI::CTransform2D::GetAnchorPreset, + "SetAnchorPreset", &UI::CTransform2D::SetAnchorPreset + ); + p_luaState.new_usertype("Light", sol::base_classes, sol::bases(), "GetColor", &CPointLight::GetColor, diff --git a/Sources/OvEditor/include/OvEditor/Core/Context.h b/Sources/OvEditor/include/OvEditor/Core/Context.h index a46fa17fb..fa9ca8610 100644 --- a/Sources/OvEditor/include/OvEditor/Core/Context.h +++ b/Sources/OvEditor/include/OvEditor/Core/Context.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -96,6 +97,7 @@ namespace OvEditor::Core OvCore::ResourceManagement::TextureManager textureManager; OvCore::ResourceManagement::ShaderManager shaderManager; OvCore::ResourceManagement::MaterialManager materialManager; + OvCore::ResourceManagement::FontManager fontManager; OvCore::ResourceManagement::SoundManager soundManager; OvWindowing::Settings::WindowSettings windowSettings; diff --git a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h index 901594d57..5382d71bf 100644 --- a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h +++ b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h @@ -177,6 +177,22 @@ namespace OvEditor::Core * Returns the current gizmo operation */ EGizmoOperation GetGizmoOperation() const; + + /** + * Sets whether the scene view renders in-game UI in screen space + * @param p_enabled + */ + void SetSceneUIRenderingEnabled(bool p_enabled); + + /** + * Toggles screen-space rendering mode for in-game UI in scene view + */ + void ToggleSceneUIRendering(); + + /** + * Returns whether scene view renders in-game UI in screen space + */ + bool IsSceneUIRenderingEnabled() const; #pragma endregion #pragma region ACTOR_CREATION_DESTRUCTION @@ -511,6 +527,7 @@ namespace OvEditor::Core OvTools::Eventing::Event ActorUnselectedEvent; OvTools::Eventing::Event EditorModeChangedEvent; OvTools::Eventing::Event EditorOperationChanged; + OvTools::Eventing::Event SceneUIRenderingChangedEvent; OvTools::Eventing::Event<> PlayEvent; private: @@ -519,6 +536,7 @@ namespace OvEditor::Core EActorSpawnMode m_actorSpawnMode = EActorSpawnMode::ORIGIN; EEditorMode m_editorMode = EEditorMode::EDIT; + bool m_sceneUIRenderingEnabled = false; std::vector>> m_delayedActions; diff --git a/Sources/OvEditor/include/OvEditor/Core/GizmoBehaviour.h b/Sources/OvEditor/include/OvEditor/Core/GizmoBehaviour.h index 80a481827..cdcc90f69 100644 --- a/Sources/OvEditor/include/OvEditor/Core/GizmoBehaviour.h +++ b/Sources/OvEditor/include/OvEditor/Core/GizmoBehaviour.h @@ -42,8 +42,15 @@ namespace OvEditor::Core * @param p_cameraPosition * @param p_operation * @param p_direction + * @param p_overrideWorldPosition */ - void StartPicking(OvCore::ECS::Actor& p_target, const OvMaths::FVector3& p_cameraPosition, EGizmoOperation p_operation, EDirection p_direction); + void StartPicking( + OvCore::ECS::Actor& p_target, + const OvMaths::FVector3& p_cameraPosition, + EGizmoOperation p_operation, + EDirection p_direction, + const OvMaths::FVector3* p_overrideWorldPosition = nullptr + ); /** * Stops the gizmo picking behaviour @@ -143,6 +150,7 @@ namespace OvEditor::Core EDirection m_direction; OvMaths::FTransform m_originalTransform; OvMaths::FVector3 m_initialOffset; + OvMaths::FVector2 m_originalUIPosition = OvMaths::FVector2::Zero; OvMaths::FVector2 m_originMouse; OvMaths::FVector2 m_currentMouse; OvMaths::FVector2 m_screenDirection; diff --git a/Sources/OvEditor/include/OvEditor/Panels/GameView.h b/Sources/OvEditor/include/OvEditor/Panels/GameView.h index 03d495676..8dfb7ff29 100644 --- a/Sources/OvEditor/include/OvEditor/Panels/GameView.h +++ b/Sources/OvEditor/include/OvEditor/Panels/GameView.h @@ -30,14 +30,17 @@ namespace OvEditor::Panels /** * Returns the main camera used by the attached scene */ - virtual OvRendering::Entities::Camera* GetCamera(); + virtual OvRendering::Entities::Camera* GetCamera() override; /** * Returns the scene used by this view */ - virtual OvCore::SceneSystem::Scene* GetScene(); + virtual OvCore::SceneSystem::Scene* GetScene() override; + + protected: + virtual OvCore::Rendering::SceneRenderer::SceneDescriptor CreateSceneDescriptor() override; private: OvCore::SceneSystem::SceneManager& m_sceneManager; }; -} \ No newline at end of file +} diff --git a/Sources/OvEditor/include/OvEditor/Panels/Toolbar.h b/Sources/OvEditor/include/OvEditor/Panels/Toolbar.h index b6456c472..985af43f2 100644 --- a/Sources/OvEditor/include/OvEditor/Panels/Toolbar.h +++ b/Sources/OvEditor/include/OvEditor/Panels/Toolbar.h @@ -37,5 +37,6 @@ namespace OvEditor::Panels OvUI::Widgets::Buttons::ButtonImage* m_pauseButton; OvUI::Widgets::Buttons::ButtonImage* m_stopButton; OvUI::Widgets::Buttons::ButtonImage* m_nextButton; + OvUI::Widgets::Buttons::ButtonImage* m_sceneUIButton; }; -} \ No newline at end of file +} diff --git a/Sources/OvEditor/premake5.lua b/Sources/OvEditor/premake5.lua index 5c7a94f4d..3b85ce6fa 100644 --- a/Sources/OvEditor/premake5.lua +++ b/Sources/OvEditor/premake5.lua @@ -60,9 +60,10 @@ project "OvEditor" "OvWindowing", -- Dependencies that others depend on - must come after + "clay", "assimp", "glfw", - } + } filter { "configurations:Debug" } defines { "DEBUG", "_DEBUG" } @@ -132,6 +133,7 @@ project "OvEditor" outputdir .. "%{cfg.buildcfg}/assimp/libassimp.a", outputdir .. "%{cfg.buildcfg}/tinyxml2/libtinyxml2.a", outputdir .. "%{cfg.buildcfg}/glad/libglad.a", + outputdir .. "%{cfg.buildcfg}/clay/libclay.a", "-Wl,--no-whole-archive", "-Wl,--allow-multiple-definition", -- Tracy and Bullet3 have some duplicate symbols } diff --git a/Sources/OvEditor/src/OvEditor/Core/Context.cpp b/Sources/OvEditor/src/OvEditor/Core/Context.cpp index 72f29b65d..eb6ed919d 100644 --- a/Sources/OvEditor/src/OvEditor/Core/Context.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/Context.cpp @@ -85,6 +85,7 @@ OvEditor::Core::Context::Context(const std::filesystem::path& p_projectFolder) : TextureManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); ShaderManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); MaterialManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); + FontManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); SoundManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); materialManager.ProvideStandardShaderDefinition({ @@ -168,6 +169,7 @@ OvEditor::Core::Context::Context(const std::filesystem::path& p_projectFolder) : ServiceLocator::Provide(textureManager); ServiceLocator::Provide(shaderManager); ServiceLocator::Provide(materialManager); + ServiceLocator::Provide(fontManager); ServiceLocator::Provide(soundManager); ServiceLocator::Provide(*inputManager); ServiceLocator::Provide(*window); @@ -185,6 +187,7 @@ OvEditor::Core::Context::~Context() textureManager.UnloadResources(); shaderManager.UnloadResources(); materialManager.UnloadResources(); + fontManager.UnloadResources(); soundManager.UnloadResources(); } diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 27d38924a..0d6051135 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -1030,6 +1030,25 @@ OvEditor::Core::EGizmoOperation OvEditor::Core::EditorActions::GetGizmoOperation return sceneView.GetGizmoOperation(); } +void OvEditor::Core::EditorActions::SetSceneUIRenderingEnabled(bool p_enabled) +{ + if (m_sceneUIRenderingEnabled != p_enabled) + { + m_sceneUIRenderingEnabled = p_enabled; + SceneUIRenderingChangedEvent.Invoke(m_sceneUIRenderingEnabled); + } +} + +void OvEditor::Core::EditorActions::ToggleSceneUIRendering() +{ + SetSceneUIRenderingEnabled(!m_sceneUIRenderingEnabled); +} + +bool OvEditor::Core::EditorActions::IsSceneUIRenderingEnabled() const +{ + return m_sceneUIRenderingEnabled; +} + OvMaths::FVector3 OvEditor::Core::EditorActions::CalculateActorSpawnPoint(float p_distanceToCamera) { auto& sceneView = m_panelsManager.GetPanelAs("Scene View"); diff --git a/Sources/OvEditor/src/OvEditor/Core/GizmoBehaviour.cpp b/Sources/OvEditor/src/OvEditor/Core/GizmoBehaviour.cpp index 96a7bfe86..62ea191da 100644 --- a/Sources/OvEditor/src/OvEditor/Core/GizmoBehaviour.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/GizmoBehaviour.cpp @@ -6,6 +6,8 @@ #include +#include + #include "OvEditor/Core/GizmoBehaviour.h" #include "OvEditor/Core/EditorActions.h" #include "OvEditor/Settings/EditorSettings.h" @@ -32,15 +34,34 @@ bool OvEditor::Core::GizmoBehaviour::IsSnappedBehaviourEnabled() const return inputManager->GetKeyState(EKey::KEY_LEFT_CONTROL) == EKeyState::KEY_DOWN || inputManager->GetKeyState(EKey::KEY_RIGHT_CONTROL) == EKeyState::KEY_DOWN; } -void OvEditor::Core::GizmoBehaviour::StartPicking(OvCore::ECS::Actor& p_target, const OvMaths::FVector3& p_cameraPosition, EGizmoOperation p_operation, EDirection p_direction) +void OvEditor::Core::GizmoBehaviour::StartPicking( + OvCore::ECS::Actor& p_target, + const OvMaths::FVector3& p_cameraPosition, + EGizmoOperation p_operation, + EDirection p_direction, + const OvMaths::FVector3* p_overrideWorldPosition +) { m_target = &p_target; m_firstMouse = true; m_firstPick = true; m_originalTransform = p_target.transform.GetFTransform(); - m_distanceToActor = OvMaths::FVector3::Distance(p_cameraPosition, m_target->transform.GetWorldPosition()); + if (p_overrideWorldPosition) + { + m_originalTransform.SetWorldPosition(*p_overrideWorldPosition); + } + m_distanceToActor = OvMaths::FVector3::Distance(p_cameraPosition, m_originalTransform.GetWorldPosition()); m_currentOperation = p_operation; m_direction = p_direction; + + if (const auto* transform2D = p_target.GetComponent()) + { + m_originalUIPosition = transform2D->GetPosition(); + } + else + { + m_originalUIPosition = OvMaths::FVector2::Zero; + } } void OvEditor::Core::GizmoBehaviour::StopPicking() @@ -122,6 +143,35 @@ OvMaths::FVector2 OvEditor::Core::GizmoBehaviour::GetScreenDirection(const OvMat void OvEditor::Core::GizmoBehaviour::ApplyTranslation(const OvMaths::FMatrix4& p_viewMatrix, const OvMaths::FMatrix4& p_projectionMatrix, const OvMaths::FVector3& p_cameraPosition, const OvMaths::FVector2& p_viewSize) { + if (auto* transform2D = m_target->GetComponent()) + { + auto axisDirection = GetScreenDirection(p_viewMatrix, p_projectionMatrix, p_viewSize); + auto totalDisplacement = m_currentMouse - m_originMouse; + totalDisplacement.y *= -1.0f; + auto translationPixels = OvMaths::FVector2::Dot(totalDisplacement, axisDirection); + + if (IsSnappedBehaviourEnabled()) + { + translationPixels = SnapValue(translationPixels, OvEditor::Settings::EditorSettings::TranslationSnapUnit); + } + + OvMaths::FVector2 axisDelta = OvMaths::FVector2::Zero; + switch (m_direction) + { + case EDirection::X: + axisDelta.x = translationPixels; + break; + case EDirection::Y: + axisDelta.y = translationPixels; + break; + case EDirection::Z: + break; + } + + transform2D->SetPosition(m_originalUIPosition + axisDelta); + return; + } + auto ray = GetMouseRay(m_currentMouse, p_viewMatrix, p_projectionMatrix, p_viewSize); const OvMaths::FVector3 planeTangent = OvMaths::FVector3::Cross(GetRealDirection(true), m_target->transform.GetWorldPosition() - p_cameraPosition); diff --git a/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp b/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp index b98b11902..272dd984f 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include @@ -809,6 +810,30 @@ namespace } }; + class FontContextualMenu : public FileContextualMenu + { + public: + FontContextualMenu(const std::string& p_filePath, bool p_protected = false) : FileContextualMenu(p_filePath, p_protected) {} + + virtual void CreateList() override + { + auto& reloadAction = CreateWidget("Reload"); + + reloadAction.ClickedEvent += [this] + { + auto& fontManager = OVSERVICE(OvCore::ResourceManagement::FontManager); + const std::string resourcePath = EDITOR_EXEC(GetResourcePath(filePath.string(), m_protected)); + if (fontManager.IsResourceRegistered(resourcePath)) + { + fontManager.AResourceManager::ReloadResource(resourcePath); + EDITOR_PANEL(OvEditor::Panels::Inspector, "Inspector").Refresh(); + } + }; + + FileContextualMenu::CreateList(); + } + }; + class EmbeddedFileContextualMenu : public OvUI::Plugins::ContextualMenu { public: @@ -887,6 +912,7 @@ namespace case TEXTURE: return p_root.AddPlugin(path, p_protected); case SHADER: return p_root.AddPlugin(path, p_protected); case MATERIAL: return p_root.AddPlugin(path, p_protected); + case FONT: return p_root.AddPlugin(path, p_protected); default: return p_root.AddPlugin(path, p_protected); } } diff --git a/Sources/OvEditor/src/OvEditor/Panels/GameView.cpp b/Sources/OvEditor/src/OvEditor/Panels/GameView.cpp index c2cff8600..e7c8dc88c 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/GameView.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/GameView.cpp @@ -47,3 +47,11 @@ OvCore::SceneSystem::Scene* OvEditor::Panels::GameView::GetScene() return m_sceneManager.GetCurrentScene(); } +OvCore::Rendering::SceneRenderer::SceneDescriptor OvEditor::Panels::GameView::CreateSceneDescriptor() +{ + auto descriptor = AView::CreateSceneDescriptor(); + descriptor.includeUI = true; + descriptor.renderUIInScreenSpace = true; + return descriptor; +} + diff --git a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp index 09273b619..c0e7f6b45 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp @@ -26,6 +26,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -66,6 +73,20 @@ namespace void AddComponent(Actor& p_actor) const override { + if constexpr ( + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v + ) + { + if (!p_actor.GetComponent()) + { + p_actor.AddComponent(); + } + } + p_actor.AddComponent(); } @@ -75,6 +96,14 @@ namespace { return !p_actor.GetComponent(); } + else if constexpr ( + std::is_same_v || + std::is_same_v || + std::is_same_v + ) + { + return !p_actor.GetComponent(); + } else { return !p_actor.GetComponent(); @@ -105,6 +134,13 @@ namespace CreateComponentInfo("Audio Listener"), CreateComponentInfo("Post Process Stack"), CreateComponentInfo("Reflection Probe"), + CreateComponentInfo("Canvas"), + CreateComponentInfo("Horizontal Layout"), + CreateComponentInfo("Image"), + CreateComponentInfo("Layout Group"), + CreateComponentInfo("Text"), + CreateComponentInfo("Transform 2D"), + CreateComponentInfo("Vertical Layout"), }); } diff --git a/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp b/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp index 3cb964f59..97adee77a 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp @@ -4,7 +4,15 @@ * @licence: MIT */ +#include +#include + #include +#include +#include +#include +#include +#include #include #include @@ -17,6 +25,18 @@ namespace { + OvMaths::FVector2 GetResolvedElementSize( + const OvCore::ECS::Components::UI::CTransform2D& p_transform2D, + const OvMaths::FVector2& p_elementSize + ) + { + const auto sizeOverride = p_transform2D.GetSize(); + return { + sizeOverride.x > 0.0f ? sizeOverride.x : std::max(p_elementSize.x, 0.0f), + sizeOverride.y > 0.0f ? sizeOverride.y : std::max(p_elementSize.y, 0.0f) + }; + } + OvTools::Utils::OptRef GetActorFromPickingResult( OvEditor::Rendering::PickingRenderPass::PickingResult p_result ) @@ -31,6 +51,70 @@ namespace return std::nullopt; } + + OvMaths::FVector3 TransformPoint(const OvMaths::FMatrix4& p_matrix, const OvMaths::FVector2& p_point) + { + const auto result = p_matrix * OvMaths::FVector4{ p_point.x, p_point.y, 0.0f, 1.0f }; + return { result.x, result.y, result.z }; + } + + std::optional ResolveUIGizmoOrigin( + OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_renderSize, + bool p_renderUIInScreenSpace + ) + { + auto* transform2D = p_actor.GetComponent(); + if (!transform2D) + { + return std::nullopt; + } + + const auto* canvas = OvCore::Rendering::UIRenderingUtils::FindCanvas(p_actor); + if (!canvas) + { + return std::nullopt; + } + + OvMaths::FVector2 elementSize = OvMaths::FVector2::Zero; + if (const auto* image = p_actor.GetComponent()) + { + elementSize = image->GetSize(); + } + else if (const auto* text = p_actor.GetComponent()) + { + elementSize = text->GetSize(); + } + else if (const auto* canvasComponent = p_actor.GetComponent()) + { + elementSize = OvCore::Rendering::UIRenderingUtils::GetCanvasSize(*canvasComponent, p_renderSize); + } + else + { + elementSize = transform2D->GetSize(); + } + + const auto canvasSize = OvCore::Rendering::UIRenderingUtils::GetCanvasSize(p_actor, p_renderSize); + const auto layoutOffset = OvCore::Rendering::UIRenderingUtils::GetLayoutOffset(p_actor); + const auto canvasScale = OvCore::Rendering::UIRenderingUtils::GetCanvasScale(*canvas, p_renderSize); + const auto worldScale = OvCore::Rendering::UIRenderingUtils::GetUIWorldScale(*canvas, p_renderUIInScreenSpace); + const auto unitsScale = p_renderUIInScreenSpace ? canvasScale : canvasScale * worldScale; + + auto matrix = transform2D->GetMatrix(canvasSize, layoutOffset, elementSize); + if (elementSize.x > 0.0f && elementSize.y > 0.0f) + { + const auto resolvedSize = GetResolvedElementSize(*transform2D, elementSize); + + matrix = matrix * OvMaths::FMatrix4::Scaling({ + resolvedSize.x / elementSize.x, + resolvedSize.y / elementSize.y, + 1.0f + }); + } + matrix = OvMaths::FMatrix4::Scaling({ unitsScale, unitsScale, 1.0f }) * matrix; + + return TransformPoint(matrix, OvMaths::FVector2::Zero); + } } OvEditor::Panels::SceneView::SceneView @@ -148,6 +232,8 @@ OvCore::Rendering::SceneRenderer::SceneDescriptor OvEditor::Panels::SceneView::C { auto descriptor = AViewControllable::CreateSceneDescriptor(); descriptor.fallbackMaterial = m_fallbackMaterial; + descriptor.includeUI = true; + descriptor.renderUIInScreenSpace = EDITOR_EXEC(IsSceneUIRenderingEnabled()); if (Settings::EditorSettings::DebugFrustumCulling) { @@ -222,11 +308,25 @@ void OvEditor::Panels::SceneView::HandleActorPicking() { if (m_highlightedGizmoDirection) { + auto& selectedActor = EDITOR_EXEC(GetSelectedActor()); + auto [winWidth, winHeight] = GetSafeSize(); + const auto renderSize = OvMaths::FVector2{ + winWidth > 0 ? static_cast(winWidth) : 1.0f, + winHeight > 0 ? static_cast(winHeight) : 1.0f + }; + const auto uiGizmoOrigin = ResolveUIGizmoOrigin( + selectedActor, + renderSize, + EDITOR_EXEC(IsSceneUIRenderingEnabled()) + ); + m_gizmoOperations.StartPicking( - EDITOR_EXEC(GetSelectedActor()), + selectedActor, m_camera.GetPosition(), m_currentOperation, - m_highlightedGizmoDirection.value()); + m_highlightedGizmoDirection.value(), + uiGizmoOrigin ? &uiGizmoOrigin.value() : nullptr + ); } else if (m_highlightedActor) { diff --git a/Sources/OvEditor/src/OvEditor/Panels/Toolbar.cpp b/Sources/OvEditor/src/OvEditor/Panels/Toolbar.cpp index 16050c417..495bfc294 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Toolbar.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Toolbar.cpp @@ -92,6 +92,19 @@ OvEditor::Panels::Toolbar::Toolbar m_nextButton->ClickedEvent += EDITOR_BIND(NextFrame); refreshButton.ClickedEvent += EDITOR_BIND(RefreshScripts); + CreateWidget(0).lineBreak = false; + m_sceneUIButton = &CreateWidget(editorResources->GetTexture("Font")->GetTexture().GetID(), iconSize); + m_sceneUIButton->lineBreak = false; + m_sceneUIButton->tooltip = "Toggle Scene View UI screen-space mode"; + m_sceneUIButton->ClickedEvent += []() { EDITOR_EXEC(ToggleSceneUIRendering()); }; + + auto updateSceneUIRendering = [this](bool p_enabled) { + m_sceneUIButton->tint = GetButtonTint(p_enabled); + }; + + updateSceneUIRendering(EDITOR_EXEC(IsSceneUIRenderingEnabled())); + EDITOR_EVENT(SceneUIRenderingChangedEvent) += updateSceneUIRendering; + EDITOR_EVENT(EditorModeChangedEvent) += [this](Core::EditorActions::EEditorMode p_mode) { using enum Core::EditorActions::EEditorMode; m_playButton->disabled = !(p_mode == EDIT || p_mode == FRAME_BY_FRAME || p_mode == PAUSE); diff --git a/Sources/OvEditor/src/OvEditor/Rendering/DebugSceneRenderer.cpp b/Sources/OvEditor/src/OvEditor/Rendering/DebugSceneRenderer.cpp index f05d274f4..6347acfaa 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/DebugSceneRenderer.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/DebugSceneRenderer.cpp @@ -4,6 +4,9 @@ * @licence: MIT */ +#include +#include + #include #include #include @@ -12,8 +15,14 @@ #include #include #include +#include +#include +#include +#include +#include #include #include +#include #include @@ -44,12 +53,15 @@ namespace const OvMaths::FVector3 kLightVolumeColor = { 1.0f, 1.0f, 0.0f }; const OvMaths::FVector3 kColliderColor = { 0.0f, 1.0f, 0.0f }; const OvMaths::FVector3 kFrustumColor = { 1.0f, 1.0f, 1.0f }; + const OvMaths::FVector3 kCanvasBoundsColor = { 1.0f, 0.35f, 0.0f }; + const OvMaths::FVector3 kUIBoundsColor = { 0.0f, 0.75f, 1.0f }; const OvMaths::FVector4 kHoveredOutlineColor{ 1.0f, 1.0f, 0.0f, 1.0f }; const OvMaths::FVector4 kSelectedOutlineColor{ 1.0f, 0.7f, 0.0f, 1.0f }; constexpr float kHoveredOutlineWidth = 2.5f; constexpr float kSelectedOutlineWidth = 5.0f; + constexpr float kUIBoundsWidth = 1.5f; OvMaths::FMatrix4 CalculateUnscaledModelMatrix(OvCore::ECS::Actor& p_actor) { @@ -109,6 +121,144 @@ namespace return lightBuffer; } + + const OvCore::ECS::Components::UI::CCanvas* FindCanvas(const OvCore::ECS::Actor& p_owner) + { + return OvCore::Rendering::UIRenderingUtils::FindCanvas(p_owner); + } + + OvMaths::FVector2 GetCanvasSize(const OvCore::ECS::Actor& p_owner, const OvMaths::FVector2& p_renderSize) + { + return OvCore::Rendering::UIRenderingUtils::GetCanvasSize(p_owner, p_renderSize); + } + + float GetCanvasScale(const OvCore::ECS::Actor& p_owner, const OvMaths::FVector2& p_renderSize) + { + if (const auto* canvas = FindCanvas(p_owner)) + { + return OvCore::Rendering::UIRenderingUtils::GetCanvasScale(*canvas, p_renderSize); + } + + return 1.0f; + } + + OvMaths::FVector2 GetLayoutOffset(const OvCore::ECS::Actor& p_owner) + { + return OvCore::Rendering::UIRenderingUtils::GetLayoutOffset(p_owner); + } + + OvMaths::FVector3 TransformPoint(const OvMaths::FMatrix4& p_matrix, const OvMaths::FVector2& p_point) + { + const auto result = p_matrix * OvMaths::FVector4{ p_point.x, p_point.y, 0.0f, 1.0f }; + return { result.x, result.y, result.z }; + } + + float GetUIWorldScale( + const OvCore::Rendering::SceneRenderer::SceneDescriptor& p_sceneDescriptor, + const OvCore::ECS::Components::UI::CCanvas& p_canvas + ) + { + return OvCore::Rendering::UIRenderingUtils::GetUIWorldScale(p_canvas, p_sceneDescriptor.renderUIInScreenSpace); + } + + OvMaths::FVector2 GetResolvedElementSize( + const OvCore::ECS::Components::UI::CTransform2D* p_transform2D, + const OvMaths::FVector2& p_elementSize + ) + { + if (!p_transform2D) + { + return { + std::max(p_elementSize.x, 0.0f), + std::max(p_elementSize.y, 0.0f) + }; + } + + const auto sizeOverride = p_transform2D->GetSize(); + return { + sizeOverride.x > 0.0f ? sizeOverride.x : std::max(p_elementSize.x, 0.0f), + sizeOverride.y > 0.0f ? sizeOverride.y : std::max(p_elementSize.y, 0.0f) + }; + } + + void ApplyTransform2DSizeOverride( + OvMaths::FMatrix4& p_matrix, + const OvCore::ECS::Components::UI::CTransform2D* p_transform2D, + const OvMaths::FVector2& p_elementSize + ) + { + if (!p_transform2D || p_elementSize.x <= 0.0f || p_elementSize.y <= 0.0f) + { + return; + } + + const auto resolvedSize = GetResolvedElementSize(p_transform2D, p_elementSize); + p_matrix = p_matrix * OvMaths::FMatrix4::Scaling({ + resolvedSize.x / p_elementSize.x, + resolvedSize.y / p_elementSize.y, + 1.0f + }); + } + + bool TryGetUIActorGizmoTransform( + const OvCore::Rendering::SceneRenderer::SceneDescriptor& p_sceneDescriptor, + const OvRendering::Data::FrameDescriptor& p_frameDescriptor, + OvCore::ECS::Actor& p_actor, + OvMaths::FVector3& p_position, + OvMaths::FQuaternion& p_rotation + ) + { + if (!p_sceneDescriptor.includeUI) + { + return false; + } + + auto* transform2D = p_actor.GetComponent(); + const auto* canvas = transform2D ? FindCanvas(p_actor) : nullptr; + if (!transform2D || !canvas) + { + return false; + } + + OvMaths::FVector2 elementSize = OvMaths::FVector2::Zero; + if (auto* image = p_actor.GetComponent()) + { + elementSize = image->GetSize(); + } + else if (auto* text = p_actor.GetComponent()) + { + elementSize = text->GetSize(); + } + else if (auto* canvasComponent = p_actor.GetComponent()) + { + elementSize = OvCore::Rendering::UIRenderingUtils::GetCanvasSize(*canvasComponent, { + static_cast(p_frameDescriptor.renderWidth), + static_cast(p_frameDescriptor.renderHeight) + }); + } + else + { + elementSize = transform2D->GetSize(); + } + + const auto renderSize = OvMaths::FVector2{ + static_cast(p_frameDescriptor.renderWidth), + static_cast(p_frameDescriptor.renderHeight) + }; + const auto canvasSize = GetCanvasSize(p_actor, renderSize); + const auto layoutOffset = GetLayoutOffset(p_actor); + const auto canvasScale = GetCanvasScale(p_actor, renderSize); + auto matrix = transform2D->GetMatrix(canvasSize, layoutOffset, elementSize); + ApplyTransform2DSizeOverride(matrix, transform2D, elementSize); + + const auto worldScale = GetUIWorldScale(p_sceneDescriptor, *canvas); + const auto unitsScale = p_sceneDescriptor.renderUIInScreenSpace ? canvasScale : canvasScale * worldScale; + matrix = OvMaths::FMatrix4::Scaling({ unitsScale, unitsScale, 1.0f }) * matrix; + + p_position = TransformPoint(matrix, OvMaths::FVector2::Zero); + p_rotation = p_actor.transform.GetWorldRotation(); + return true; + } } class DebugCamerasRenderPass : public OvRendering::Core::ARenderPass @@ -332,17 +482,26 @@ class DebugActorRenderPass : public OvRendering::Core::ARenderPass const bool isActorHovered = debugSceneDescriptor.highlightedActor && debugSceneDescriptor.highlightedActor->GetID() == selectedActor.GetID(); DrawActorDebugElements(selectedActor); + auto gizmoPosition = selectedActor.transform.GetWorldPosition(); + auto gizmoRotation = selectedActor.transform.GetWorldRotation(); + TryGetUIActorGizmoTransform( + m_renderer.GetDescriptor(), + m_renderer.GetFrameDescriptor(), + selectedActor, + gizmoPosition, + gizmoRotation + ); m_renderer.GetFeature().DrawOutline( selectedActor, isActorHovered ? - kHoveredOutlineColor : - kSelectedOutlineColor, + kHoveredOutlineColor : + kSelectedOutlineColor, kSelectedOutlineWidth ); m_renderer.Clear(false, true, false, OvMaths::FVector3::Zero); m_renderer.GetFeature().DrawGizmo( - selectedActor.transform.GetWorldPosition(), - selectedActor.transform.GetWorldRotation(), + gizmoPosition, + gizmoRotation, debugSceneDescriptor.gizmoOperation, false, debugSceneDescriptor.highlightedGizmoDirection @@ -365,6 +524,11 @@ class DebugActorRenderPass : public OvRendering::Core::ARenderPass { if (p_actor.IsActive()) { + if (auto layout = p_actor.GetComponent()) + { + layout->ApplyControlledChildrenSizes(); + } + /* Render static mesh outline and bounding spheres */ if (OvEditor::Settings::EditorSettings::ShowGeometryBounds) { @@ -416,6 +580,21 @@ class DebugActorRenderPass : public OvRendering::Core::ARenderPass } } + if (auto image = p_actor.GetComponent()) + { + DrawUIBounds(p_actor, image->GetSize()); + } + + if (auto text = p_actor.GetComponent()) + { + DrawUIBounds(p_actor, text->GetSize()); + } + + if (auto canvas = p_actor.GetComponent()) + { + DrawCanvasBounds(p_actor, *canvas); + } + for (auto& child : p_actor.GetChildren()) { DrawActorDebugElements(*child); @@ -423,6 +602,107 @@ class DebugActorRenderPass : public OvRendering::Core::ARenderPass } } + void DrawUIBounds(OvCore::ECS::Actor& p_actor, const OvMaths::FVector2& p_size) + { + if (p_size.x <= 0.0f || p_size.y <= 0.0f) + { + return; + } + + const auto& sceneDescriptor = m_renderer.GetDescriptor(); + if (!sceneDescriptor.includeUI) + { + return; + } + + const auto* canvas = FindCanvas(p_actor); + if (!canvas) + { + return; + } + + const auto& frameDescriptor = m_renderer.GetFrameDescriptor(); + const auto renderSize = OvMaths::FVector2{ + static_cast(frameDescriptor.renderWidth), + static_cast(frameDescriptor.renderHeight) + }; + + auto* transform2D = p_actor.GetComponent(); + const auto resolvedSize = GetResolvedElementSize(transform2D, p_size); + const auto canvasSize = GetCanvasSize(p_actor, renderSize); + const auto layoutOffset = GetLayoutOffset(p_actor); + const auto canvasScale = GetCanvasScale(p_actor, renderSize); + auto matrix = transform2D ? + transform2D->GetMatrix(canvasSize, layoutOffset, p_size) : + p_actor.transform.GetFTransform().GetWorldMatrix(); + ApplyTransform2DSizeOverride(matrix, transform2D, p_size); + + const auto worldScale = GetUIWorldScale(sceneDescriptor, *canvas); + const auto unitsScale = sceneDescriptor.renderUIInScreenSpace ? canvasScale : canvasScale * worldScale; + matrix = OvMaths::FMatrix4::Scaling({ unitsScale, unitsScale, 1.0f }) * matrix; + + const auto halfSize = resolvedSize * 0.5f; + const auto corners = std::to_array({ + TransformPoint(matrix, { -halfSize.x, -halfSize.y }), + TransformPoint(matrix, { halfSize.x, -halfSize.y }), + TransformPoint(matrix, { halfSize.x, halfSize.y }), + TransformPoint(matrix, { -halfSize.x, halfSize.y }) + }); + + auto pso = m_renderer.CreatePipelineState(); + m_debugShapeFeature.DrawLine(pso, corners[0], corners[1], kUIBoundsColor, kUIBoundsWidth, false); + m_debugShapeFeature.DrawLine(pso, corners[1], corners[2], kUIBoundsColor, kUIBoundsWidth, false); + m_debugShapeFeature.DrawLine(pso, corners[2], corners[3], kUIBoundsColor, kUIBoundsWidth, false); + m_debugShapeFeature.DrawLine(pso, corners[3], corners[0], kUIBoundsColor, kUIBoundsWidth, false); + } + + void DrawCanvasBounds(OvCore::ECS::Actor& p_actor, OvCore::ECS::Components::UI::CCanvas& p_canvas) + { + const auto& sceneDescriptor = m_renderer.GetDescriptor(); + if (!sceneDescriptor.includeUI) + { + return; + } + + const auto& frameDescriptor = m_renderer.GetFrameDescriptor(); + const auto renderSize = OvMaths::FVector2{ + static_cast(frameDescriptor.renderWidth), + static_cast(frameDescriptor.renderHeight) + }; + + const auto canvasSize = GetCanvasSize(p_actor, renderSize); + if (canvasSize.x <= 0.0f || canvasSize.y <= 0.0f) + { + return; + } + + auto* transform2D = p_actor.GetComponent(); + const auto layoutOffset = GetLayoutOffset(p_actor); + const auto canvasScale = GetCanvasScale(p_actor, renderSize); + auto matrix = transform2D ? + transform2D->GetMatrix(canvasSize, layoutOffset, canvasSize) : + p_actor.transform.GetFTransform().GetWorldMatrix(); + ApplyTransform2DSizeOverride(matrix, transform2D, canvasSize); + + const auto worldScale = GetUIWorldScale(sceneDescriptor, p_canvas); + const auto unitsScale = sceneDescriptor.renderUIInScreenSpace ? canvasScale : canvasScale * worldScale; + matrix = OvMaths::FMatrix4::Scaling({ unitsScale, unitsScale, 1.0f }) * matrix; + + const auto halfSize = canvasSize * 0.5f; + const auto corners = std::to_array({ + TransformPoint(matrix, { -halfSize.x, -halfSize.y }), + TransformPoint(matrix, { halfSize.x, -halfSize.y }), + TransformPoint(matrix, { halfSize.x, halfSize.y }), + TransformPoint(matrix, { -halfSize.x, halfSize.y }) + }); + + auto pso = m_renderer.CreatePipelineState(); + m_debugShapeFeature.DrawLine(pso, corners[0], corners[1], kCanvasBoundsColor, kUIBoundsWidth, false); + m_debugShapeFeature.DrawLine(pso, corners[1], corners[2], kCanvasBoundsColor, kUIBoundsWidth, false); + m_debugShapeFeature.DrawLine(pso, corners[2], corners[3], kCanvasBoundsColor, kUIBoundsWidth, false); + m_debugShapeFeature.DrawLine(pso, corners[3], corners[0], kCanvasBoundsColor, kUIBoundsWidth, false); + } + void DrawFrustumLines( const OvMaths::FVector3& pos, const OvMaths::FVector3& forward, diff --git a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp index 688c03e91..1d87c718f 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp @@ -4,15 +4,21 @@ * @licence: MIT */ +#include #include #include #include #include +#include +#include +#include +#include #include #include #include #include +#include #include #include @@ -44,6 +50,138 @@ namespace p_material.SetProperty(p_uniformName, color, true); } } + + const OvCore::ECS::Components::UI::CCanvas* FindCanvas(const OvCore::ECS::Actor& p_owner) + { + return OvCore::Rendering::UIRenderingUtils::FindCanvas(p_owner); + } + + OvMaths::FVector2 GetCanvasSize(const OvCore::ECS::Actor& p_owner, const OvMaths::FVector2& p_renderSize) + { + return OvCore::Rendering::UIRenderingUtils::GetCanvasSize(p_owner, p_renderSize); + } + + float GetCanvasScale(const OvCore::ECS::Actor& p_owner, const OvMaths::FVector2& p_renderSize) + { + if (const auto* canvas = FindCanvas(p_owner)) + { + return OvCore::Rendering::UIRenderingUtils::GetCanvasScale(*canvas, p_renderSize); + } + + return 1.0f; + } + + OvMaths::FVector2 GetLayoutOffset(const OvCore::ECS::Actor& p_owner) + { + return OvCore::Rendering::UIRenderingUtils::GetLayoutOffset(p_owner); + } + + OvMaths::FVector3 TransformPoint(const OvMaths::FMatrix4& p_matrix, const OvMaths::FVector2& p_point) + { + const auto result = p_matrix * OvMaths::FVector4{ p_point.x, p_point.y, 0.0f, 1.0f }; + return { result.x, result.y, result.z }; + } + + OvMaths::FVector2 GetResolvedElementSize( + const OvCore::ECS::Components::UI::CTransform2D* p_transform2D, + const OvMaths::FVector2& p_elementSize + ) + { + if (!p_transform2D) + { + return { + std::max(p_elementSize.x, 0.0f), + std::max(p_elementSize.y, 0.0f) + }; + } + + const auto sizeOverride = p_transform2D->GetSize(); + return { + sizeOverride.x > 0.0f ? sizeOverride.x : std::max(p_elementSize.x, 0.0f), + sizeOverride.y > 0.0f ? sizeOverride.y : std::max(p_elementSize.y, 0.0f) + }; + } + + void ApplyTransform2DSizeOverride( + OvMaths::FMatrix4& p_matrix, + const OvCore::ECS::Components::UI::CTransform2D* p_transform2D, + const OvMaths::FVector2& p_elementSize + ) + { + if (!p_transform2D || p_elementSize.x <= 0.0f || p_elementSize.y <= 0.0f) + { + return; + } + + const auto resolvedSize = GetResolvedElementSize(p_transform2D, p_elementSize); + p_matrix = p_matrix * OvMaths::FMatrix4::Scaling({ + resolvedSize.x / p_elementSize.x, + resolvedSize.y / p_elementSize.y, + 1.0f + }); + } + + bool TryGetUIActorGizmoTransform( + const bool p_includeUI, + const bool p_renderUIInScreenSpace, + const uint32_t p_renderWidth, + const uint32_t p_renderHeight, + OvCore::ECS::Actor& p_actor, + OvMaths::FVector3& p_position, + OvMaths::FQuaternion& p_rotation + ) + { + if (!p_includeUI) + { + return false; + } + + auto* transform2D = p_actor.GetComponent(); + const auto* canvas = transform2D ? FindCanvas(p_actor) : nullptr; + if (!transform2D || !canvas) + { + return false; + } + + OvMaths::FVector2 elementSize = OvMaths::FVector2::Zero; + if (auto* image = p_actor.GetComponent()) + { + elementSize = image->GetSize(); + } + else if (auto* text = p_actor.GetComponent()) + { + elementSize = text->GetSize(); + } + else if (auto* canvasComponent = p_actor.GetComponent()) + { + elementSize = OvCore::Rendering::UIRenderingUtils::GetCanvasSize(*canvasComponent, { + static_cast(p_renderWidth), + static_cast(p_renderHeight) + }); + } + else + { + elementSize = transform2D->GetSize(); + } + + const auto renderSize = OvMaths::FVector2{ + static_cast(p_renderWidth), + static_cast(p_renderHeight) + }; + const auto canvasSize = GetCanvasSize(p_actor, renderSize); + const auto layoutOffset = GetLayoutOffset(p_actor); + const auto canvasScale = GetCanvasScale(p_actor, renderSize); + auto matrix = transform2D->GetMatrix(canvasSize, layoutOffset, elementSize); + ApplyTransform2DSizeOverride(matrix, transform2D, elementSize); + + const auto worldScale = OvCore::Rendering::UIRenderingUtils::GetUIWorldScale(*canvas, p_renderUIInScreenSpace); + const auto unitsScale = p_renderUIInScreenSpace ? canvasScale : canvasScale * worldScale; + matrix = OvMaths::FMatrix4::Scaling({ unitsScale, unitsScale, 1.0f }) * matrix; + + p_position = TransformPoint(matrix, OvMaths::FVector2::Zero); + p_rotation = p_actor.transform.GetWorldRotation(); + return true; + } } OvEditor::Rendering::PickingRenderPass::PickingRenderPass(OvRendering::Core::CompositeRenderer& p_renderer) : @@ -141,11 +279,22 @@ void OvEditor::Rendering::PickingRenderPass::Draw(OvRendering::Data::PipelineSta if (debugSceneDescriptor.selectedActor) { auto& selectedActor = debugSceneDescriptor.selectedActor.value(); + auto gizmoPosition = selectedActor.transform.GetWorldPosition(); + auto gizmoRotation = selectedActor.transform.GetWorldRotation(); + TryGetUIActorGizmoTransform( + sceneDescriptor.includeUI, + sceneDescriptor.renderUIInScreenSpace, + frameDescriptor.renderWidth, + frameDescriptor.renderHeight, + selectedActor, + gizmoPosition, + gizmoRotation + ); DrawPickableGizmo( pso, - selectedActor.transform.GetWorldPosition(), - selectedActor.transform.GetWorldRotation(), + gizmoPosition, + gizmoRotation, debugSceneDescriptor.gizmoOperation ); } diff --git a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp index 6e8ae56e5..4790bb88c 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp @@ -19,6 +19,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include @@ -192,6 +198,68 @@ namespace }; } + std::function CreateImageHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) + { + return [p_parent, p_onItemClicked]() + { + auto& instance = EDITOR_EXEC(CreateEmptyActor(false, ResolveAliveParent(p_parent))); + instance.AddComponent(); + instance.AddComponent(); + instance.SetName("Image"); + + EDITOR_EXEC(SelectActor(instance)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; + } + + std::function CreateTextHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) + { + return [p_parent, p_onItemClicked]() + { + auto& instance = EDITOR_EXEC(CreateEmptyActor(false, ResolveAliveParent(p_parent))); + instance.AddComponent(); + instance.AddComponent(); + instance.SetName("Text"); + + EDITOR_EXEC(SelectActor(instance)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; + } + + template + std::function CreateLayoutHandler( + OvCore::ECS::Actor* p_parent, + const std::string& p_name, + std::optional> p_onItemClicked + ) + { + return [p_parent, p_name, p_onItemClicked]() + { + auto& instance = EDITOR_EXEC(CreateEmptyActor(false, ResolveAliveParent(p_parent))); + if (!instance.GetComponent()) + { + instance.AddComponent(); + } + instance.AddComponent(); + instance.SetName(p_name); + + EDITOR_EXEC(SelectActor(instance)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; + } + std::function CreateFromPrefabHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { return [p_parent, p_onItemClicked]() @@ -247,6 +315,7 @@ void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets auto& physicals = p_menuList.CreateWidget("Physicals"); auto& lights = p_menuList.CreateWidget("Lights"); auto& audio = p_menuList.CreateWidget("Audio"); + auto& ui = p_menuList.CreateWidget("UI"); auto& others = p_menuList.CreateWidget("Others"); primitives.CreateWidget("Cube").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Cube", p_onItemClicked); @@ -269,6 +338,12 @@ void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets lights.CreateWidget("Ambient Sphere").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); audio.CreateWidget("Audio Source").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); audio.CreateWidget("Audio Listener").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + ui.CreateWidget("Canvas").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + ui.CreateWidget("Image").ClickedEvent += CreateImageHandler(p_parent, p_onItemClicked); + ui.CreateWidget("Text").ClickedEvent += CreateTextHandler(p_parent, p_onItemClicked); + ui.CreateWidget("Horizontal Layout").ClickedEvent += CreateLayoutHandler(p_parent, "Horizontal Layout", p_onItemClicked); + ui.CreateWidget("Vertical Layout").ClickedEvent += CreateLayoutHandler(p_parent, "Vertical Layout", p_onItemClicked); + ui.CreateWidget("Transform 2D").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); others.CreateWidget("Camera").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); others.CreateWidget("Post Process Stack").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); others.CreateWidget("Reflection Probe").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); diff --git a/Sources/OvGame/include/OvGame/Core/Context.h b/Sources/OvGame/include/OvGame/Core/Context.h index 898b60093..3f24fcbba 100644 --- a/Sources/OvGame/include/OvGame/Core/Context.h +++ b/Sources/OvGame/include/OvGame/Core/Context.h @@ -17,6 +17,7 @@ #include +#include #include #include #include @@ -67,8 +68,9 @@ namespace OvGame::Core OvCore::ResourceManagement::TextureManager textureManager; OvCore::ResourceManagement::ShaderManager shaderManager; OvCore::ResourceManagement::MaterialManager materialManager; + OvCore::ResourceManagement::FontManager fontManager; OvCore::ResourceManagement::SoundManager soundManager; OvTools::Filesystem::IniFile projectSettings; }; -} \ No newline at end of file +} diff --git a/Sources/OvGame/premake5.lua b/Sources/OvGame/premake5.lua index f2f03c545..e1289f34a 100644 --- a/Sources/OvGame/premake5.lua +++ b/Sources/OvGame/premake5.lua @@ -57,8 +57,11 @@ project "OvGame" "OvRendering", "OvTools", "OvUI", - "OvWindowing" - } + "OvWindowing", + + -- Dependencies that others depend on - must come after + "clay" + } filter { "configurations:Debug" } defines { "DEBUG", "_DEBUG" } @@ -109,6 +112,7 @@ project "OvGame" outputdir .. "%{cfg.buildcfg}/assimp/libassimp.a", outputdir .. "%{cfg.buildcfg}/tinyxml2/libtinyxml2.a", outputdir .. "%{cfg.buildcfg}/glad/libglad.a", + outputdir .. "%{cfg.buildcfg}/clay/libclay.a", "-Wl,--no-whole-archive", "-Wl,--allow-multiple-definition", -- Tracy and Bullet3 have some duplicate symbols } diff --git a/Sources/OvGame/src/OvGame/Core/Context.cpp b/Sources/OvGame/src/OvGame/Core/Context.cpp index 6cd4e16ff..1248bc887 100644 --- a/Sources/OvGame/src/OvGame/Core/Context.cpp +++ b/Sources/OvGame/src/OvGame/Core/Context.cpp @@ -57,6 +57,7 @@ OvGame::Core::Context::Context() : TextureManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); ShaderManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); MaterialManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); + FontManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); SoundManager::ProvideAssetPaths(projectAssetsPath, engineAssetsPath); materialManager.ProvideStandardShaderDefinition({ @@ -161,6 +162,7 @@ OvGame::Core::Context::Context() : ServiceLocator::Provide(textureManager); ServiceLocator::Provide(shaderManager); ServiceLocator::Provide(materialManager); + ServiceLocator::Provide(fontManager); ServiceLocator::Provide(soundManager); ServiceLocator::Provide(*inputManager); ServiceLocator::Provide(*window); @@ -184,5 +186,6 @@ OvGame::Core::Context::~Context() textureManager.UnloadResources(); shaderManager.UnloadResources(); materialManager.UnloadResources(); + fontManager.UnloadResources(); soundManager.UnloadResources(); } diff --git a/Sources/OvRendering/include/OvRendering/Resources/Font.h b/Sources/OvRendering/include/OvRendering/Resources/Font.h new file mode 100644 index 000000000..11b567996 --- /dev/null +++ b/Sources/OvRendering/include/OvRendering/Resources/Font.h @@ -0,0 +1,178 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace OvRendering::Resources +{ + class Texture; + + /** + * Font resource baked into a static glyph atlas + */ + class Font + { + public: + struct Glyph + { + float xOffset = 0.0f; + float yOffset = 0.0f; + float xAdvance = 0.0f; + float width = 0.0f; + float height = 0.0f; + float uMin = 0.0f; + float vMin = 0.0f; + float uMax = 0.0f; + float vMax = 0.0f; + }; + + static constexpr uint32_t kFirstGlyph = 32; + static constexpr uint32_t kGlyphCount = 95; + + /** + * Constructor + * @param p_path + * @param p_realPath + */ + Font(const std::string& p_path, const std::filesystem::path& p_realPath); + + /** + * Destructor + */ + ~Font(); + + Font(const Font&) = delete; + Font& operator=(const Font&) = delete; + + /** + * Reloads the font from disk + * @param p_realPath + */ + bool Reload(const std::filesystem::path& p_realPath); + + /** + * Ensures a baked atlas variant exists for the given pixel size and makes it active + * @param p_pixelSize + */ + bool SetActivePixelSize(float p_pixelSize); + + /** + * Ensures a baked atlas variant exists for the given pixel size + * @param p_pixelSize + */ + bool EnsurePixelSize(float p_pixelSize); + + /** + * Returns true if the font has a valid atlas + */ + bool IsValid() const; + + /** + * Returns the glyph atlas font size + */ + float GetPixelSize() const; + + /** + * Returns the glyph atlas font size for the given requested pixel size + */ + float GetPixelSize(float p_pixelSize) const; + + /** + * Returns the line height in atlas pixels + */ + float GetLineHeight() const; + + /** + * Returns the line height in atlas pixels for the given requested pixel size + */ + float GetLineHeight(float p_pixelSize) const; + + /** + * Returns the glyph associated with the given ASCII character + * @param p_character + */ + const Glyph* GetGlyph(char p_character) const; + + /** + * Returns the glyph associated with the given ASCII character and requested pixel size + * @param p_character + * @param p_pixelSize + */ + const Glyph* GetGlyph(char p_character, float p_pixelSize) const; + + /** + * Returns the static glyph atlas texture + */ + Texture* GetAtlasTexture() const; + + /** + * Returns the static glyph atlas texture for the given requested pixel size + */ + Texture* GetAtlasTexture(float p_pixelSize); + + /** + * Initializes or refreshes the embedded material used for text rendering + * @param p_shader + */ + bool EnsureEmbeddedMaterial(Shader* p_shader); + + /** + * Initializes or refreshes the embedded material used for text rendering at the given pixel size + * @param p_shader + * @param p_pixelSize + */ + bool EnsureEmbeddedMaterial(Shader* p_shader, float p_pixelSize); + + /** + * Returns the embedded material used for text rendering + */ + Data::Material* GetEmbeddedMaterial() const; + + /** + * Returns the embedded material used for text rendering at the given pixel size + */ + Data::Material* GetEmbeddedMaterial(float p_pixelSize); + + public: + const std::string path; + + private: + struct AtlasVariant + { + bool valid = false; + float pixelSize = 32.0f; + float lineHeight = 32.0f; + uint32_t atlasWidth = 0; + uint32_t atlasHeight = 0; + std::array glyphs = {}; + Texture* atlasTexture = nullptr; + std::unique_ptr embeddedMaterial; + }; + + AtlasVariant* GetActiveVariant(); + const AtlasVariant* GetActiveVariant() const; + AtlasVariant* GetVariant(uint32_t p_pixelSize); + const AtlasVariant* GetVariant(uint32_t p_pixelSize) const; + AtlasVariant* GetOrCreateVariant(uint32_t p_pixelSize); + void DestroyAtlasVariants(); + bool CreateAtlasVariant(uint32_t p_pixelSize); + + private: + bool m_valid = false; + uint32_t m_activePixelSize = 32; + std::filesystem::path m_realPath; + std::unordered_map m_atlasVariants; + }; +} diff --git a/Sources/OvRendering/premake5.lua b/Sources/OvRendering/premake5.lua index f4c16ac50..fe4a0a8b2 100644 --- a/Sources/OvRendering/premake5.lua +++ b/Sources/OvRendering/premake5.lua @@ -17,6 +17,7 @@ project "OvRendering" includedirs { -- Dependencies dependdir .. "assimp/include", + dependdir .. "freetype/include", dependdir .. "glad/include", dependdir .. "stb_image/include", dependdir .. "tracy", diff --git a/Sources/OvRendering/src/OvRendering/Resources/Font.cpp b/Sources/OvRendering/src/OvRendering/Resources/Font.cpp new file mode 100644 index 000000000..97f512d98 --- /dev/null +++ b/Sources/OvRendering/src/OvRendering/Resources/Font.cpp @@ -0,0 +1,624 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include FT_FREETYPE_H + +namespace +{ + constexpr uint32_t kMinimumAtlasSize = 512; + constexpr uint32_t kMaximumAtlasSize = 4096; + constexpr float kDefaultPixelSize = 32.0f; + constexpr float kMinimumPixelSize = 1.0f; + constexpr float kMaximumPixelSize = 256.0f; + constexpr uint32_t kGlyphPadding = 1; + constexpr const char* kFontAtlasUniform = "u_FontAtlas"; + + uint32_t ToPixelSizeKey(float p_pixelSize) + { + if (!std::isfinite(p_pixelSize)) + { + return static_cast(kDefaultPixelSize); + } + + const auto clamped = std::clamp(p_pixelSize, kMinimumPixelSize, kMaximumPixelSize); + return std::max(1, static_cast(std::lround(clamped))); + } + + void ConfigureEmbeddedMaterial(OvRendering::Data::Material& p_material) + { + p_material.SetOrthographicSupport(true); + p_material.SetPerspectiveSupport(true); + p_material.SetBlendable(true); + p_material.SetUserInterface(true); + p_material.SetBackfaceCulling(false); + p_material.SetFrontfaceCulling(false); + p_material.SetDepthTest(false); + p_material.SetDepthWriting(false); + p_material.SetColorWriting(true); + p_material.SetCastShadows(false); + p_material.SetReceiveShadows(false); + p_material.SetCapturedByReflectionProbes(false); + p_material.SetReceiveReflections(false); + p_material.SetGPUInstances(1); + } + + struct BakedFont + { + bool valid = false; + float pixelSize = kDefaultPixelSize; + float lineHeight = kDefaultPixelSize; + uint32_t atlasWidth = 0; + uint32_t atlasHeight = 0; + std::array glyphs = {}; + std::vector atlasData; + }; + + struct FreeTypeLibrary + { + FT_Library handle = nullptr; + + FreeTypeLibrary() + { + if (FT_Init_FreeType(&handle) != 0) + { + handle = nullptr; + } + } + + ~FreeTypeLibrary() + { + if (handle) + { + FT_Done_FreeType(handle); + } + } + }; + + struct FreeTypeFace + { + FT_Face handle = nullptr; + + ~FreeTypeFace() + { + if (handle) + { + FT_Done_Face(handle); + } + } + }; + + std::vector ReadFile(const std::filesystem::path& p_path) + { + std::ifstream file{ p_path, std::ios::binary | std::ios::ate }; + if (!file) + { + return {}; + } + + const auto size = file.tellg(); + if (size <= std::streampos{ 0 }) + { + return {}; + } + + std::vector data(static_cast(size)); + file.seekg(0, std::ios::beg); + file.read(reinterpret_cast(data.data()), static_cast(data.size())); + + return file ? data : std::vector{}; + } + + bool CopyGlyphBitmap( + const FT_Bitmap& p_bitmap, + uint32_t p_x, + uint32_t p_y, + uint32_t p_atlasWidth, + std::vector& p_alphaAtlas + ) + { + if (p_bitmap.pixel_mode != FT_PIXEL_MODE_GRAY) + { + return false; + } + + const uint32_t glyphWidth = static_cast(p_bitmap.width); + const uint32_t glyphHeight = static_cast(p_bitmap.rows); + const int32_t pitch = static_cast(p_bitmap.pitch); + const uint32_t pitchAbs = static_cast(pitch >= 0 ? pitch : -pitch); + + for (uint32_t row = 0; row < glyphHeight; ++row) + { + const uint32_t sourceRow = pitch >= 0 ? row : glyphHeight - 1 - row; + const uint8_t* source = p_bitmap.buffer + static_cast(sourceRow) * pitchAbs; + const size_t destinationOffset = (static_cast(p_y) + row) * p_atlasWidth + p_x; + std::copy(source, source + glyphWidth, p_alphaAtlas.begin() + destinationOffset); + } + + return true; + } + + BakedFont BakeFont(const std::filesystem::path& p_realPath, float p_pixelSize) + { + using namespace OvRendering::Resources; + + BakedFont result; + + FreeTypeLibrary library; + if (!library.handle) + { + OVLOG_WARNING("Unable to initialize FreeType"); + return result; + } + + const auto fontData = ReadFile(p_realPath); + if (fontData.empty()) + { + OVLOG_WARNING("Unable to read font: " + p_realPath.string()); + return result; + } + + FreeTypeFace face; + if (FT_New_Memory_Face(library.handle, fontData.data(), static_cast(fontData.size()), 0, &face.handle) != 0) + { + OVLOG_WARNING("Unable to initialize font: " + p_realPath.string()); + return result; + } + + if (FT_Select_Charmap(face.handle, FT_ENCODING_UNICODE) != 0) + { + OVLOG_WARNING("Font does not provide a Unicode charmap: " + p_realPath.string()); + return result; + } + + if (FT_Set_Pixel_Sizes(face.handle, 0, static_cast(ToPixelSizeKey(p_pixelSize))) != 0) + { + OVLOG_WARNING("Unable to set font pixel size: " + p_realPath.string()); + return result; + } + + result.pixelSize = static_cast(ToPixelSizeKey(p_pixelSize)); + + if (face.handle->size) + { + result.lineHeight = static_cast(face.handle->size->metrics.height) / 64.0f; + } + + for (uint32_t atlasSize = kMinimumAtlasSize; atlasSize <= kMaximumAtlasSize; atlasSize *= 2) + { + std::vector alphaAtlas(static_cast(atlasSize) * atlasSize); + uint32_t cursorX = 0; + uint32_t cursorY = 0; + uint32_t rowHeight = 0; + bool fits = true; + + for (uint32_t i = 0; i < Font::kGlyphCount; ++i) + { + const uint32_t character = Font::kFirstGlyph + i; + if (FT_Load_Char(face.handle, character, FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL) != 0) + { + OVLOG_WARNING("Unable to load glyph from font atlas: " + p_realPath.string()); + return result; + } + + const FT_GlyphSlot glyphSlot = face.handle->glyph; + const FT_Bitmap& bitmap = glyphSlot->bitmap; + const uint32_t glyphWidth = static_cast(bitmap.width); + const uint32_t glyphHeight = static_cast(bitmap.rows); + auto& glyph = result.glyphs[i]; + + glyph.xOffset = static_cast(glyphSlot->bitmap_left); + glyph.yOffset = -static_cast(glyphSlot->bitmap_top); + glyph.xAdvance = static_cast(glyphSlot->advance.x) / 64.0f; + glyph.width = static_cast(glyphWidth); + glyph.height = static_cast(glyphHeight); + + if (glyphWidth == 0 || glyphHeight == 0) + { + continue; + } + + if (cursorX + glyphWidth + kGlyphPadding > atlasSize) + { + cursorX = 0; + cursorY += rowHeight + kGlyphPadding; + rowHeight = 0; + } + + if (cursorY + glyphHeight + kGlyphPadding > atlasSize) + { + fits = false; + break; + } + + if (!CopyGlyphBitmap(bitmap, cursorX, cursorY, atlasSize, alphaAtlas)) + { + OVLOG_WARNING("Unsupported glyph bitmap format in font: " + p_realPath.string()); + return result; + } + + glyph.uMin = static_cast(cursorX) / static_cast(atlasSize); + glyph.vMin = static_cast(cursorY) / static_cast(atlasSize); + glyph.uMax = static_cast(cursorX + glyphWidth) / static_cast(atlasSize); + glyph.vMax = static_cast(cursorY + glyphHeight) / static_cast(atlasSize); + + cursorX += glyphWidth + kGlyphPadding; + rowHeight = std::max(rowHeight, glyphHeight); + } + + if (!fits) + { + continue; + } + + result.atlasWidth = atlasSize; + result.atlasHeight = atlasSize; + result.atlasData.resize(static_cast(atlasSize) * atlasSize * 4); + + for (size_t i = 0; i < alphaAtlas.size(); ++i) + { + const size_t offset = i * 4; + result.atlasData[offset + 0] = 255; + result.atlasData[offset + 1] = 255; + result.atlasData[offset + 2] = 255; + result.atlasData[offset + 3] = alphaAtlas[i]; + } + + result.valid = true; + return result; + } + + OVLOG_WARNING("Font atlas is too small for: " + p_realPath.string()); + return result; + } +} + +OvRendering::Resources::Font::Font(const std::string& p_path, const std::filesystem::path& p_realPath) : + path(p_path) +{ + Reload(p_realPath); +} + +OvRendering::Resources::Font::~Font() +{ + DestroyAtlasVariants(); +} + +bool OvRendering::Resources::Font::Reload(const std::filesystem::path& p_realPath) +{ + m_realPath = p_realPath; + m_activePixelSize = ToPixelSizeKey(kDefaultPixelSize); + m_valid = false; + DestroyAtlasVariants(); + m_atlasVariants.clear(); + + return SetActivePixelSize(static_cast(m_activePixelSize)); +} + +bool OvRendering::Resources::Font::SetActivePixelSize(float p_pixelSize) +{ + const auto pixelSize = ToPixelSizeKey(p_pixelSize); + auto* variant = GetOrCreateVariant(pixelSize); + if (!variant) + { + m_valid = false; + return false; + } + + m_activePixelSize = pixelSize; + m_valid = variant->valid && variant->atlasTexture; + return m_valid; +} + +bool OvRendering::Resources::Font::EnsurePixelSize(float p_pixelSize) +{ + return GetOrCreateVariant(ToPixelSizeKey(p_pixelSize)) != nullptr; +} + +bool OvRendering::Resources::Font::IsValid() const +{ + const auto* variant = GetActiveVariant(); + return m_valid && variant && variant->atlasTexture; +} + +float OvRendering::Resources::Font::GetPixelSize() const +{ + if (const auto* variant = GetActiveVariant(); variant) + { + return variant->pixelSize; + } + + return static_cast(m_activePixelSize); +} + +float OvRendering::Resources::Font::GetPixelSize(float p_pixelSize) const +{ + if (const auto* variant = GetVariant(ToPixelSizeKey(p_pixelSize)); variant) + { + return variant->pixelSize; + } + + return static_cast(ToPixelSizeKey(p_pixelSize)); +} + +float OvRendering::Resources::Font::GetLineHeight() const +{ + if (const auto* variant = GetActiveVariant(); variant) + { + return variant->lineHeight; + } + + return static_cast(m_activePixelSize); +} + +float OvRendering::Resources::Font::GetLineHeight(float p_pixelSize) const +{ + if (const auto* variant = GetVariant(ToPixelSizeKey(p_pixelSize)); variant) + { + return variant->lineHeight; + } + + return static_cast(ToPixelSizeKey(p_pixelSize)); +} + +const OvRendering::Resources::Font::Glyph* OvRendering::Resources::Font::GetGlyph(char p_character) const +{ + const auto character = static_cast(p_character); + if (character < kFirstGlyph || character >= kFirstGlyph + kGlyphCount) + { + return nullptr; + } + + if (const auto* variant = GetActiveVariant(); variant) + { + return &variant->glyphs[character - kFirstGlyph]; + } + + return nullptr; +} + +const OvRendering::Resources::Font::Glyph* OvRendering::Resources::Font::GetGlyph(char p_character, float p_pixelSize) const +{ + const auto character = static_cast(p_character); + if (character < kFirstGlyph || character >= kFirstGlyph + kGlyphCount) + { + return nullptr; + } + + const auto* variant = GetVariant(ToPixelSizeKey(p_pixelSize)); + return variant ? &variant->glyphs[character - kFirstGlyph] : nullptr; +} + +OvRendering::Resources::Texture* OvRendering::Resources::Font::GetAtlasTexture() const +{ + if (const auto* variant = GetActiveVariant(); variant) + { + return variant->atlasTexture; + } + + return nullptr; +} + +OvRendering::Resources::Texture* OvRendering::Resources::Font::GetAtlasTexture(float p_pixelSize) +{ + if (auto* variant = GetOrCreateVariant(ToPixelSizeKey(p_pixelSize)); variant) + { + return variant->atlasTexture; + } + + return nullptr; +} + +bool OvRendering::Resources::Font::EnsureEmbeddedMaterial(Shader* p_shader) +{ + return EnsureEmbeddedMaterial(p_shader, static_cast(m_activePixelSize)); +} + +bool OvRendering::Resources::Font::EnsureEmbeddedMaterial(Shader* p_shader, float p_pixelSize) +{ + auto* variant = GetOrCreateVariant(ToPixelSizeKey(p_pixelSize)); + if (!variant) + { + return false; + } + + if (!p_shader || !variant->atlasTexture) + { + variant->embeddedMaterial.reset(); + return false; + } + + if (!variant->embeddedMaterial) + { + variant->embeddedMaterial = std::make_unique(p_shader); + ConfigureEmbeddedMaterial(*variant->embeddedMaterial); + } + else if (variant->embeddedMaterial->GetShader() != p_shader) + { + variant->embeddedMaterial->SetShader(p_shader); + ConfigureEmbeddedMaterial(*variant->embeddedMaterial); + } + + if (!variant->embeddedMaterial->IsValid()) + { + return false; + } + + variant->embeddedMaterial->TrySetProperty(kFontAtlasUniform, variant->atlasTexture); + return true; +} + +OvRendering::Data::Material* OvRendering::Resources::Font::GetEmbeddedMaterial() const +{ + if (const auto* variant = GetActiveVariant(); variant) + { + return variant->embeddedMaterial.get(); + } + + return nullptr; +} + +OvRendering::Data::Material* OvRendering::Resources::Font::GetEmbeddedMaterial(float p_pixelSize) +{ + if (auto* variant = GetOrCreateVariant(ToPixelSizeKey(p_pixelSize)); variant) + { + return variant->embeddedMaterial.get(); + } + + return nullptr; +} + +OvRendering::Resources::Font::AtlasVariant* OvRendering::Resources::Font::GetActiveVariant() +{ + return GetVariant(m_activePixelSize); +} + +const OvRendering::Resources::Font::AtlasVariant* OvRendering::Resources::Font::GetActiveVariant() const +{ + return GetVariant(m_activePixelSize); +} + +OvRendering::Resources::Font::AtlasVariant* OvRendering::Resources::Font::GetVariant(uint32_t p_pixelSize) +{ + const auto found = m_atlasVariants.find(p_pixelSize); + return found != m_atlasVariants.end() ? &found->second : nullptr; +} + +const OvRendering::Resources::Font::AtlasVariant* OvRendering::Resources::Font::GetVariant(uint32_t p_pixelSize) const +{ + const auto found = m_atlasVariants.find(p_pixelSize); + return found != m_atlasVariants.end() ? &found->second : nullptr; +} + +OvRendering::Resources::Font::AtlasVariant* OvRendering::Resources::Font::GetOrCreateVariant(uint32_t p_pixelSize) +{ + if (auto* variant = GetVariant(p_pixelSize)) + { + return variant->valid ? variant : nullptr; + } + + if (!CreateAtlasVariant(p_pixelSize)) + { + auto& failedVariant = m_atlasVariants[p_pixelSize]; + failedVariant.valid = false; + failedVariant.pixelSize = static_cast(p_pixelSize); + return nullptr; + } + + if (auto* variant = GetVariant(p_pixelSize)) + { + return variant->valid ? variant : nullptr; + } + + return nullptr; +} + +void OvRendering::Resources::Font::DestroyAtlasVariants() +{ + for (auto& [_, variant] : m_atlasVariants) + { + Loaders::TextureLoader::Destroy(variant.atlasTexture); + variant.atlasTexture = nullptr; + variant.atlasWidth = 0; + variant.atlasHeight = 0; + variant.embeddedMaterial.reset(); + variant.valid = false; + } +} + +bool OvRendering::Resources::Font::CreateAtlasVariant(uint32_t p_pixelSize) +{ + using namespace OvRendering::Settings; + + if (m_realPath.empty()) + { + return false; + } + + auto bakedFont = BakeFont(m_realPath, static_cast(p_pixelSize)); + if (!bakedFont.valid) + { + return false; + } + + auto it = m_atlasVariants.find(p_pixelSize); + if (it == m_atlasVariants.end()) + { + it = m_atlasVariants.emplace(p_pixelSize, AtlasVariant{}).first; + } + + auto& variant = it->second; + + if (variant.atlasTexture) + { + if (variant.atlasWidth == bakedFont.atlasWidth && variant.atlasHeight == bakedFont.atlasHeight) + { + Loaders::TextureLoader::ReloadFromMemory( + *variant.atlasTexture, + bakedFont.atlasData.data(), + bakedFont.atlasWidth, + bakedFont.atlasHeight, + ETextureFilteringMode::LINEAR, + ETextureFilteringMode::LINEAR, + ETextureWrapMode::CLAMP_TO_EDGE, + ETextureWrapMode::CLAMP_TO_EDGE, + false + ); + } + else + { + Loaders::TextureLoader::Destroy(variant.atlasTexture); + variant.atlasTexture = nullptr; + } + } + + if (!variant.atlasTexture) + { + variant.atlasTexture = Loaders::TextureLoader::CreateFromMemory( + bakedFont.atlasData.data(), + bakedFont.atlasWidth, + bakedFont.atlasHeight, + ETextureFilteringMode::LINEAR, + ETextureFilteringMode::LINEAR, + ETextureWrapMode::CLAMP_TO_EDGE, + ETextureWrapMode::CLAMP_TO_EDGE, + false + ); + + if (!variant.atlasTexture) + { + OVLOG_WARNING("Unable to create font atlas texture: " + m_realPath.string()); + return false; + } + } + + variant.valid = true; + variant.pixelSize = bakedFont.pixelSize; + variant.lineHeight = bakedFont.lineHeight; + variant.atlasWidth = bakedFont.atlasWidth; + variant.atlasHeight = bakedFont.atlasHeight; + variant.glyphs = bakedFont.glyphs; + if (variant.embeddedMaterial && variant.embeddedMaterial->IsValid()) + { + variant.embeddedMaterial->TrySetProperty(kFontAtlasUniform, variant.atlasTexture); + } + + return true; +} diff --git a/Sources/OvTools/include/OvTools/Eventing/Event.h b/Sources/OvTools/include/OvTools/Eventing/Event.h index e7d41b6f8..3f37cdda2 100644 --- a/Sources/OvTools/include/OvTools/Eventing/Event.h +++ b/Sources/OvTools/include/OvTools/Eventing/Event.h @@ -8,6 +8,8 @@ #include #include +#include +#include namespace OvTools::Eventing { @@ -77,4 +79,4 @@ namespace OvTools::Eventing }; } -#include "OvTools/Eventing/Event.inl" \ No newline at end of file +#include "OvTools/Eventing/Event.inl" diff --git a/Sources/OvTools/include/OvTools/Eventing/Event.inl b/Sources/OvTools/include/OvTools/Eventing/Event.inl index 664d2e103..b1e9e02a7 100644 --- a/Sources/OvTools/include/OvTools/Eventing/Event.inl +++ b/Sources/OvTools/include/OvTools/Eventing/Event.inl @@ -51,7 +51,20 @@ namespace OvTools::Eventing template void Event::Invoke(ArgTypes... p_args) { - for (auto const& [key, value] : m_callbacks) - value(p_args...); + std::vector callbacks; + callbacks.reserve(m_callbacks.size()); + + for (const auto& pair : m_callbacks) + { + callbacks.push_back(pair.second); + } + + for (const auto& callback : callbacks) + { + if (callback) + { + callback(p_args...); + } + } } -} \ No newline at end of file +} diff --git a/Sources/OvTools/src/OvTools/Utils/PathParser.cpp b/Sources/OvTools/src/OvTools/Utils/PathParser.cpp index 3195eb67a..acfe66457 100644 --- a/Sources/OvTools/src/OvTools/Utils/PathParser.cpp +++ b/Sources/OvTools/src/OvTools/Utils/PathParser.cpp @@ -117,6 +117,7 @@ OvTools::Utils::PathParser::EFileType OvTools::Utils::PathParser::StringToFileTy if (p_type == "Material") return EFileType::MATERIAL; if (p_type == "Sound") return EFileType::SOUND; if (p_type == "Prefab") return EFileType::PREFAB; + if (p_type == "Font") return EFileType::FONT; return EFileType::UNKNOWN; } @@ -134,7 +135,7 @@ OvTools::Utils::PathParser::EFileType OvTools::Utils::PathParser::GetFileType(co else if (ext == "ovscene") return EFileType::SCENE; else if (ext == "ovprefab") return EFileType::PREFAB; else if (ext == "lua" || ext == "ovscript") return EFileType::SCRIPT; - else if (ext == "ttf") return EFileType::FONT; + else if (ext == "ttf" || ext == "otf") return EFileType::FONT; return EFileType::UNKNOWN; } diff --git a/Sources/OvUI/include/OvUI/Internal/WidgetContainer.h b/Sources/OvUI/include/OvUI/Internal/WidgetContainer.h index 839f26401..08455a23b 100644 --- a/Sources/OvUI/include/OvUI/Internal/WidgetContainer.h +++ b/Sources/OvUI/include/OvUI/Internal/WidgetContainer.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include "OvUI/Widgets/AWidget.h" @@ -88,5 +89,6 @@ namespace OvUI::Internal protected: std::vector> m_widgets; bool m_reversedDrawOrder = false; + uint32_t m_drawCallDepth = 0; }; } diff --git a/Sources/OvUI/include/OvUI/Plugins/Pluginable.h b/Sources/OvUI/include/OvUI/Plugins/Pluginable.h index af0fc02d0..553b74226 100644 --- a/Sources/OvUI/include/OvUI/Plugins/Pluginable.h +++ b/Sources/OvUI/include/OvUI/Plugins/Pluginable.h @@ -6,6 +6,9 @@ #pragma once +#include +#include +#include #include #include "OvUI/Plugins/IPlugin.h" @@ -18,6 +21,12 @@ namespace OvUI::Plugins class Pluginable { public: + Pluginable() = default; + Pluginable(const Pluginable&) = delete; + Pluginable& operator=(const Pluginable&) = delete; + Pluginable(Pluginable&&) = delete; + Pluginable& operator=(Pluginable&&) = delete; + /** * Destructor (Destroys every plugins) */ @@ -48,9 +57,9 @@ namespace OvUI::Plugins { static_assert(std::is_base_of::value, "T should derive from IPlugin"); - for (auto it = m_plugins.begin(); it != m_plugins.end(); ++it) + for (size_t i = 0; i < m_plugins.size(); ++i) { - T* result = dynamic_cast(*it); + T* result = dynamic_cast(m_plugins[i]); if (result) return result; } @@ -81,4 +90,4 @@ namespace OvUI::Plugins private: std::vector m_plugins; }; -} \ No newline at end of file +} diff --git a/Sources/OvUI/include/OvUI/Widgets/InputFields/InputText.h b/Sources/OvUI/include/OvUI/Widgets/InputFields/InputText.h index d65df79fa..920f6ce19 100644 --- a/Sources/OvUI/include/OvUI/Widgets/InputFields/InputText.h +++ b/Sources/OvUI/include/OvUI/Widgets/InputFields/InputText.h @@ -34,8 +34,10 @@ namespace OvUI::Widgets::InputFields bool selectAllOnClick = false; bool focusOnNextDraw = false; bool fullWidth = false; + bool multiline = false; + float multilineHeight = 0.0f; uint32_t iconTextureID = 0; OvTools::Eventing::Event ContentChangedEvent; OvTools::Eventing::Event EnterPressedEvent; }; -} \ No newline at end of file +} diff --git a/Sources/OvUI/src/OvUI/Internal/WidgetContainer.cpp b/Sources/OvUI/src/OvUI/Internal/WidgetContainer.cpp index 449fceab9..066250823 100644 --- a/Sources/OvUI/src/OvUI/Internal/WidgetContainer.cpp +++ b/Sources/OvUI/src/OvUI/Internal/WidgetContainer.cpp @@ -24,6 +24,17 @@ void OvUI::Internal::WidgetContainer::RemoveWidget(Widgets::AWidget& p_widget) if (found != m_widgets.end()) { + if (m_drawCallDepth > 0) + { + if (found->first) + { + found->first->SetParent(nullptr); + found->first->Destroy(); + } + + return; + } + if (found->second == Internal::EMemoryMode::INTERNAL_MANAGMENT) delete found->first; @@ -33,6 +44,20 @@ void OvUI::Internal::WidgetContainer::RemoveWidget(Widgets::AWidget& p_widget) void OvUI::Internal::WidgetContainer::RemoveAllWidgets() { + if (m_drawCallDepth > 0) + { + for (auto& pair : m_widgets) + { + if (pair.first) + { + pair.first->SetParent(nullptr); + pair.first->Destroy(); + } + } + + return; + } + std::for_each(m_widgets.begin(), m_widgets.end(), [](auto& pair) { if (pair.second == Internal::EMemoryMode::INTERNAL_MANAGMENT) @@ -68,8 +93,13 @@ void OvUI::Internal::WidgetContainer::CollectGarbages() { bool toDestroy = p_item.first && p_item.first->IsDestroyed(); - if (toDestroy && p_item.second == Internal::EMemoryMode::INTERNAL_MANAGMENT) - delete p_item.first; + if (toDestroy) + { + p_item.first->SetParent(nullptr); + + if (p_item.second == Internal::EMemoryMode::INTERNAL_MANAGMENT) + delete p_item.first; + } return toDestroy; }), m_widgets.end()); @@ -88,20 +118,35 @@ void OvUI::Internal::WidgetContainer::DrawWidgets() widgetsToDraw.reserve(m_widgets.size()); std::ranges::copy(m_widgets | std::views::keys, std::back_inserter(widgetsToDraw)); + ++m_drawCallDepth; + if (m_reversedDrawOrder) [[unlikely]] { for (WidgetType widget : widgetsToDraw | std::views::reverse) { - widget->Draw(); + if (widget && !widget->IsDestroyed()) + { + widget->Draw(); + } } } else { for (WidgetType widget : widgetsToDraw) { - widget->Draw(); + if (widget && !widget->IsDestroyed()) + { + widget->Draw(); + } } } + + --m_drawCallDepth; + + if (m_drawCallDepth == 0) + { + CollectGarbages(); + } } void OvUI::Internal::WidgetContainer::ReverseDrawOrder(const bool reversed) diff --git a/Sources/OvUI/src/OvUI/Widgets/InputFields/InputText.cpp b/Sources/OvUI/src/OvUI/Widgets/InputFields/InputText.cpp index c13d49c61..3bab39417 100644 --- a/Sources/OvUI/src/OvUI/Widgets/InputFields/InputText.cpp +++ b/Sources/OvUI/src/OvUI/Widgets/InputFields/InputText.cpp @@ -4,6 +4,7 @@ * @licence: MIT */ +#include #include #include @@ -50,8 +51,39 @@ void OvUI::Widgets::InputFields::InputText::_Draw_Impl() if (needFocus) ImGui::SetKeyboardFocusHere(0); - content.resize(256, '\0'); - bool enterPressed = ImGui::InputText((label + m_widgetID).c_str(), &content[0], 256, ImGuiInputTextFlags_EnterReturnsTrue | (selectAllOnClick ? ImGuiInputTextFlags_AutoSelectAll : 0)); + constexpr size_t kSingleLineBufferSize = 256; + constexpr size_t kMultilineBufferSize = 4096; + const size_t bufferSize = multiline ? kMultilineBufferSize : kSingleLineBufferSize; + content.resize(bufferSize, '\0'); + + const auto commonFlags = selectAllOnClick ? ImGuiInputTextFlags_AutoSelectAll : ImGuiInputTextFlags_None; + bool enterPressed = false; + + if (multiline) + { + const float fieldHeight = multilineHeight > 0.0f ? + multilineHeight : + ImGui::GetTextLineHeightWithSpacing() * 4.0f; + const float fieldWidth = fullWidth ? -FLT_MIN : 0.0f; + + enterPressed = ImGui::InputTextMultiline( + (label + m_widgetID).c_str(), + &content[0], + bufferSize, + ImVec2(fieldWidth, fieldHeight), + commonFlags + ); + } + else + { + enterPressed = ImGui::InputText( + (label + m_widgetID).c_str(), + &content[0], + bufferSize, + ImGuiInputTextFlags_EnterReturnsTrue | commonFlags + ); + } + content = content.c_str(); if (content != previousContent) @@ -60,6 +92,6 @@ void OvUI::Widgets::InputFields::InputText::_Draw_Impl() this->NotifyChange(); } - if (enterPressed) + if (enterPressed && !multiline) EnterPressedEvent.Invoke(content); -} \ No newline at end of file +} diff --git a/Sources/OvUI/src/OvUI/Widgets/Layout/Group.cpp b/Sources/OvUI/src/OvUI/Widgets/Layout/Group.cpp index 636c564e8..d0188c1d8 100644 --- a/Sources/OvUI/src/OvUI/Widgets/Layout/Group.cpp +++ b/Sources/OvUI/src/OvUI/Widgets/Layout/Group.cpp @@ -20,17 +20,31 @@ void OvUI::Widgets::Layout::Group::_Draw_Impl() CollectGarbages(); - if (m_widgets.empty()) + std::vector widgetsToDraw; + widgetsToDraw.reserve(m_widgets.size()); + + for (auto& pair : m_widgets) + { + auto* widget = pair.first; + if (widget && !widget->IsDestroyed()) + { + widgetsToDraw.push_back(widget); + } + } + + if (widgetsToDraw.empty()) { return; } + ++m_drawCallDepth; + const auto& style = ImGui::GetStyle(); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{ style.ItemSpacing.x * 0.5f, style.CellPadding.y }); - if (ImGui::BeginTable(("group" + m_widgetID).c_str(), static_cast(m_widgets.size()), ImGuiTableFlags_NoSavedSettings)) + if (ImGui::BeginTable(("group" + m_widgetID).c_str(), static_cast(widgetsToDraw.size()), ImGuiTableFlags_NoSavedSettings)) { - for (size_t index = 0; index < m_widgets.size(); ++index) + for (size_t index = 0; index < widgetsToDraw.size(); ++index) { const auto columnFlags = static_cast(index) == stretchWidget ? ImGuiTableColumnFlags_WidthStretch : @@ -41,7 +55,7 @@ void OvUI::Widgets::Layout::Group::_Draw_Impl() ImGui::TableNextRow(); - for (size_t index = 0; index < m_widgets.size(); ++index) + for (size_t index = 0; index < widgetsToDraw.size(); ++index) { ImGui::TableSetColumnIndex(static_cast(index)); @@ -50,7 +64,7 @@ void OvUI::Widgets::Layout::Group::_Draw_Impl() ImGui::SetNextItemWidth(-FLT_MIN); } - auto& widget = *m_widgets[index].first; + auto& widget = *widgetsToDraw[index]; const auto previousLineBreak = widget.lineBreak; widget.lineBreak = true; widget.Draw(); @@ -61,4 +75,11 @@ void OvUI::Widgets::Layout::Group::_Draw_Impl() } ImGui::PopStyleVar(); + + --m_drawCallDepth; + + if (m_drawCallDepth == 0) + { + CollectGarbages(); + } } diff --git a/premake5.lua b/premake5.lua index 921dc6a03..e08ae6062 100644 --- a/premake5.lua +++ b/premake5.lua @@ -54,6 +54,7 @@ group "Dependencies" include "Dependencies/tracy" include "Dependencies/lua" include "Dependencies/freetype" + include "Dependencies/clay" include "Dependencies/glad" include "Dependencies/soloud" include "Dependencies/assimp"