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/Editor/Shaders/Gizmo.ovfx b/Resources/Editor/Shaders/Gizmo.ovfx index 62d19b6a9..b00e340ff 100644 --- a/Resources/Editor/Shaders/Gizmo.ovfx +++ b/Resources/Editor/Shaders/Gizmo.ovfx @@ -21,7 +21,9 @@ out VS_OUT uniform bool u_IsBall; uniform bool u_IsPickable; +uniform bool u_ShowZAxis; uniform int u_HighlightedAxis; +uniform float u_GizmoScale; mat4 rotationMatrix(vec3 axis, float angle) { @@ -41,6 +43,13 @@ mat4 rotationMatrix(vec3 axis, float angle) void main() { + if (!u_ShowZAxis && gl_InstanceID == 0) + { + vs_out.Color = vec3(0.0f); + gl_Position = vec4(2.0f, 2.0f, 2.0f, 1.0f); + return; + } + mat4 instanceModel = ubo_Model; if (gl_InstanceID == 1) @@ -49,10 +58,11 @@ void main() instanceModel *= rotationMatrix(vec3(1, 0, 0), radians(90)); /* Y axis */ float distanceToCamera = distance(ubo_ViewPos, instanceModel[3].xyz); + float gizmoScale = u_GizmoScale > 0.0 ? u_GizmoScale : distanceToCamera * 0.1f; vec3 pos = geo_Pos; - vec3 fragPos = vec3(instanceModel * vec4(pos * distanceToCamera * 0.1f, 1.0)); + vec3 fragPos = vec3(instanceModel * vec4(pos * gizmoScale, 1.0)); if (u_IsPickable) { @@ -110,4 +120,4 @@ uniform bool u_IsPickable; void main() { FRAGMENT_COLOR = vec4(fs_in.Color, 1.0f); -} \ No newline at end of file +} 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..67e3a2dad --- /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 display size stored by Transform UI data +---@return Vector2 +function Image:GetSize() end + +--- Defines the image display size stored by Transform UI data +---@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..667826126 --- /dev/null +++ b/Resources/Engine/Lua/Components/LayoutGroup.lua @@ -0,0 +1,102 @@ +---@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 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 + +--- Returns whether the layout expands children width +---@return boolean +function LayoutGroup:GetForceExpandWidth() end + +--- Defines whether the layout expands children width +---@param value boolean +function LayoutGroup:SetForceExpandWidth(value) end + +--- Returns whether the layout expands children height +---@return boolean +function LayoutGroup:GetForceExpandHeight() end + +--- Defines whether the layout expands children height +---@param value boolean +function LayoutGroup:SetForceExpandHeight(value) end diff --git a/Resources/Engine/Lua/Components/Text.lua b/Resources/Engine/Lua/Components/Text.lua new file mode 100644 index 000000000..d62a58084 --- /dev/null +++ b/Resources/Engine/Lua/Components/Text.lua @@ -0,0 +1,71 @@ +---@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 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/Transform.lua b/Resources/Engine/Lua/Components/Transform.lua index cdae48b31..bc41fc18e 100644 --- a/Resources/Engine/Lua/Components/Transform.lua +++ b/Resources/Engine/Lua/Components/Transform.lua @@ -4,6 +4,27 @@ ---@class Transform : Component Transform = {} +--- Defines the anchor preset used by Transform UI data +---@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 +} + --- Alias for SetLocalPosition ---@param position Vector3 function Transform:SetPosition(position) end @@ -112,6 +133,68 @@ function Transform:GetWorldUp() end ---@return Vector3 function Transform:GetWorldRight() end +--- Enables UI transform data on this transform +function Transform:EnableUIData() end + +--- Disables UI transform data on this transform +function Transform:DisableUIData() end + +--- Returns whether this transform carries UI data +---@return boolean +function Transform:HasUIData() end + +--- Returns whether this transform is currently driven by a parent Canvas hierarchy +---@return boolean +function Transform:HasActiveUIData() end + +--- Returns the anchored UI position +---@return Vector2 +function Transform:GetUIPosition() end + +--- Defines the anchored UI position +---@param position Vector2 +function Transform:SetUIPosition(position) end + +--- Returns the UI rotation in degrees +---@return number +function Transform:GetUIRotation() end + +--- Defines the UI rotation in degrees +---@param rotation number +function Transform:SetUIRotation(rotation) end + +--- Returns the UI scale +---@return Vector2 +function Transform:GetUIScale() end + +--- Defines the UI scale +---@param scale Vector2 +function Transform:SetUIScale(scale) end + +--- Returns the UI size +---@return Vector2 +function Transform:GetUISize() end + +--- Defines the UI size +---@param size Vector2 +function Transform:SetUISize(size) end + +--- Returns the normalized pivot in range [-1, 1] +---@return Vector2 +function Transform:GetUIPivot() end + +--- Defines the normalized pivot in range [-1, 1] +---@param pivot Vector2 +function Transform:SetUIPivot(pivot) end + +--- Returns the UI anchor preset +---@return AnchorPreset +function Transform:GetUIAnchorPreset() end + +--- Defines the UI anchor preset +---@param anchorPreset AnchorPreset +function Transform:SetUIAnchorPreset(anchorPreset) end + --- Returns the actor that owns this component ---@return Actor function Transform:GetOwner() 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..6648776d3 100644 --- a/Resources/Engine/Lua/Scene/Actor.lua +++ b/Resources/Engine/Lua/Scene/Actor.lua @@ -146,6 +146,30 @@ 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 Behaviour of the given type attached to this actor (If any) ---@param name string ---@return table|nil @@ -219,6 +243,30 @@ 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 + --- Removes the ModelRenderer component from the actor function Actor:RemoveModelRenderer() end --- Removes the PhysicalBox component from the actor @@ -251,7 +299,18 @@ 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 --- Adds a behaviour of given type to the actor and returns it ---@param name string ---@param scriptPath string diff --git a/Resources/Engine/Materials/Image.ovmat b/Resources/Engine/Materials/Image.ovmat new file mode 100644 index 000000000..670a6f011 --- /dev/null +++ b/Resources/Engine/Materials/Image.ovmat @@ -0,0 +1,36 @@ + + :Shaders\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/Text.ovmat b/Resources/Engine/Materials/Text.ovmat new file mode 100644 index 000000000..0ae16b062 --- /dev/null +++ b/Resources/Engine/Materials/Text.ovmat @@ -0,0 +1,36 @@ + + :Shaders\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/Image.ovfx b/Resources/Engine/Shaders/Image.ovfx new file mode 100644 index 000000000..0bcaa4e3a --- /dev/null +++ b/Resources/Engine/Shaders/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/Text.ovfx b/Resources/Engine/Shaders/Text.ovfx new file mode 100644 index 000000000..d7ac1febb --- /dev/null +++ b/Resources/Engine/Shaders/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/CTransform.h b/Sources/OvCore/include/OvCore/ECS/Components/CTransform.h index db6b3b850..fc6ca1c59 100644 --- a/Sources/OvCore/include/OvCore/ECS/Components/CTransform.h +++ b/Sources/OvCore/include/OvCore/ECS/Components/CTransform.h @@ -6,14 +6,16 @@ #pragma once +#include + #include "OvCore/ECS/Components/AComponent.h" +#include #include +#include #include #include -#include "AComponent.h" - namespace OvCore::ECS { class Actor; } namespace OvCore::ECS::Components @@ -24,6 +26,34 @@ namespace OvCore::ECS::Components class CTransform : public AComponent { public: + enum class EUIAnchorPreset + { + 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 + }; + + struct UIData + { + OvMaths::FVector2 position = OvMaths::FVector2::Zero; + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + OvMaths::FVector2 pivot = OvMaths::FVector2::Zero; + EUIAnchorPreset anchorPreset = EUIAnchorPreset::CENTER; + }; + /** * Create a transform without setting a parent * @param p_localPosition @@ -189,6 +219,33 @@ namespace OvCore::ECS::Components */ OvMaths::FVector3 GetLocalRight() const; + void EnableUIData(); + void DisableUIData(); + bool HasUIData() const; + bool HasActiveUIData() const; + const std::optional& GetUIData() const; + + void SetUIPosition(const OvMaths::FVector2& p_position); + const OvMaths::FVector2& GetUIPosition() const; + + void SetUIRotation(float p_rotation); + float GetUIRotation() const; + + void SetUIScale(const OvMaths::FVector2& p_scale); + OvMaths::FVector2 GetUIScale() const; + + void SetUISize(const OvMaths::FVector2& p_size); + const OvMaths::FVector2& GetUISize() const; + + void SetUIPivot(const OvMaths::FVector2& p_pivot); + const OvMaths::FVector2& GetUIPivot() const; + + void SetUIAnchorPreset(EUIAnchorPreset p_anchorPreset); + EUIAnchorPreset GetUIAnchorPreset() const; + + bool IsHorizontalUIPositionEditable() const; + bool IsVerticalUIPositionEditable() const; + /** * Serialize the component * @param p_doc @@ -209,8 +266,17 @@ namespace OvCore::ECS::Components */ virtual void OnInspector(OvUI::Internal::WidgetContainer& p_root) override; + private: + static EUIAnchorPreset ToUIAnchorPreset(int p_value); + static bool IsHorizontalUIPositionEditable(EUIAnchorPreset p_anchorPreset); + static bool IsVerticalUIPositionEditable(EUIAnchorPreset p_anchorPreset); + + UIData& GetOrCreateUIData(); + const UIData& GetUIDataOrDefault() const; + private: OvMaths::FTransform m_transform; + std::optional m_uiData; }; template<> @@ -218,4 +284,4 @@ namespace OvCore::ECS::Components { static constexpr std::string_view Name = "class OvCore::ECS::Components::CTransform"; }; -} \ No newline at end of file +} 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..186536337 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CImage.h @@ -0,0 +1,143 @@ +/** +* @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 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 display size stored by the owner Transform UI data + */ + OvMaths::FVector2 GetSize() const; + + /** + * Returns the stable quad size used before Transform UI/layout scaling + */ + OvMaths::FVector2 GetIntrinsicSize() 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 ValidateTextureReference(); + void UpdateIntrinsicSize(); + void RebuildMesh(); + void RefreshMaterial(); + + private: + OvRendering::Resources::Texture* m_texture = nullptr; + OvMaths::FVector4 m_tint = { 1.0f, 1.0f, 1.0f, 1.0f }; + OvMaths::FVector2 m_intrinsicSize = { 100.0f, 100.0f }; + + std::unique_ptr m_mesh; + std::unique_ptr m_material; + OvTools::Eventing::Event<> m_textureChangedEvent; + OvRendering::Resources::Texture* m_materialTexture = nullptr; + + bool m_textureReferenceDirty = true; + bool m_materialStateDirty = true; + bool m_materialTextureDirty = true; + bool m_materialTintDirty = true; + }; +} + +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..fe6848a3e --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CLayoutGroup.h @@ -0,0 +1,301 @@ +/** +* @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 +{ + class ClayLayoutSolverContext; + + /** + * Arranges direct user interface children along an axis + */ + class CLayoutGroup : public AComponent + { + public: + using ChildOffset = std::pair; + + struct ChildLayout + { + const ECS::Actor* actor = nullptr; + OvMaths::FVector2 offset = OvMaths::FVector2::Zero; + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + bool hasDirectWidth = false; + bool hasDirectHeight = false; + bool valid = false; + }; + + 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); + + /** + * Destructor + */ + ~CLayoutGroup() override; + + /** + * 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; + + /** + * Returns the actual layout container size after children, padding and Transform UI size are resolved + */ + OvMaths::FVector2 GetComputedSize() 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; + + /** + * Sets whether the layout should expand children width to fill available space + * @param p_forceExpandWidth + */ + void SetForceExpandWidth(bool p_forceExpandWidth); + + /** + * Returns whether the layout expands children width + */ + bool GetForceExpandWidth() const; + + /** + * Sets whether the layout should expand children height to fill available space + * @param p_forceExpandHeight + */ + void SetForceExpandHeight(bool p_forceExpandHeight); + + /** + * Returns whether the layout expands children height + */ + bool GetForceExpandHeight() const; + + /** + * Returns the layout offset for a direct child + * @param p_child + */ + OvMaths::FVector2 GetChildOffset(const ECS::Actor& p_child) const; + + /** + * Returns the resolved layout data for a direct child + * @param p_child + */ + std::optional GetChildLayout(const ECS::Actor& p_child) const; + + /** + * Returns the layout offsets for direct children + */ + std::vector GetChildOffsets() const; + + /** + * Returns the resolved layout data for direct children + */ + std::vector GetChildLayouts() 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; + + protected: + virtual bool IsDirectionEditable() const; + + private: + struct LayoutInputChild + { + ECS::Actor* actor = nullptr; + OvMaths::FVector2 preferredSize = OvMaths::FVector2::Zero; + }; + + struct LayoutCacheChildSignature + { + const ECS::Actor* actor = nullptr; + OvMaths::FVector2 preferredSize = OvMaths::FVector2::Zero; + }; + + struct LayoutCacheSignature + { + EDirection direction = EDirection::HORIZONTAL; + float spacing = 0.0f; + OvMaths::FVector4 padding = OvMaths::FVector4::Zero; + EHorizontalAlignment horizontalAlignment = EHorizontalAlignment::CENTER; + EVerticalAlignment verticalAlignment = EVerticalAlignment::CENTER; + bool controlChildrenWidth = false; + bool controlChildrenHeight = false; + bool forceExpandWidth = false; + bool forceExpandHeight = false; + OvMaths::FVector2 containerSize = OvMaths::FVector2::Zero; + OvMaths::FVector2 pivot = OvMaths::FVector2::Zero; + std::vector children; + }; + + struct LayoutCacheInput + { + LayoutCacheSignature signature; + std::vector children; + }; + + struct LayoutCache + { + bool valid = false; + LayoutCacheSignature signature; + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + std::vector children; + }; + + LayoutCacheInput BuildLayoutCacheInput() const; + const LayoutCache& GetResolvedLayout() const; + void InvalidateLayoutCache() const; + static bool HasSameLayoutSignature(const LayoutCacheSignature& p_lhs, const LayoutCacheSignature& p_rhs); + + EDirection m_direction = EDirection::HORIZONTAL; + float m_spacing = 0.0f; + 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; + bool m_forceExpandWidth = false; + bool m_forceExpandHeight = false; + mutable LayoutCache m_layoutCache; + mutable std::unique_ptr m_layoutSolverContext; + }; +} + +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..66f17ca71 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CText.h @@ -0,0 +1,215 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include +#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 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 mesh for a resolved UI size, or nullptr if the text cannot be rendered + */ + OvRendering::Resources::Mesh* GetMesh(const OvMaths::FVector2& p_resolvedSize) 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; + + /** + * Returns the generated text bounds size for a resolved UI size + */ + OvMaths::FVector2 GetSize(const OvMaths::FVector2& p_resolvedSize) 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 MarkMaterialSourceDirty(); + void MarkMaterialColorDirty(); + void RebuildLayout() const; + void RebuildLayout(const OvMaths::FVector2& p_uiSize) const; + void RebuildMesh() const; + void RebuildMesh(const OvMaths::FVector2& p_uiSize) const; + void RefreshMaterial(); + + private: + std::string m_text = "Text"; + std::string m_fontPath; + float m_fontSize = 32.0f; + OvMaths::FVector4 m_color = { 1.0f, 1.0f, 1.0f, 1.0f }; + EHorizontalAlignment m_horizontalAlignment = EHorizontalAlignment::LEFT; + EVerticalAlignment m_verticalAlignment = EVerticalAlignment::TOP; + mutable std::string m_unavailableFontPath; + + mutable bool m_layoutDirty = true; + mutable OvMaths::FVector2 m_lastLayoutUISize = OvMaths::FVector2::Zero; + mutable bool m_meshDirty = true; + mutable OvMaths::FVector2 m_lastMeshUISize = OvMaths::FVector2::Zero; + mutable OvMaths::FVector2 m_size = OvMaths::FVector2::Zero; + mutable TextLayoutEngine::Output m_layout; + mutable std::unique_ptr m_mesh; + std::unique_ptr m_material; + OvRendering::Resources::Font* m_materialFont = nullptr; + OvRendering::Resources::Shader* m_materialShader = nullptr; + uint64_t m_materialFontRevision = 0; + bool m_materialSourceDirty = true; + bool m_materialColorDirty = true; + }; +} + +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/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/ECS/Components/UI/ClayLayoutSolver.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/ClayLayoutSolver.h new file mode 100644 index 000000000..c882becfa --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/ClayLayoutSolver.h @@ -0,0 +1,111 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI +{ + class ClayLayoutSolverContext + { + public: + ClayLayoutSolverContext(); + ~ClayLayoutSolverContext(); + + ClayLayoutSolverContext(const ClayLayoutSolverContext&) = delete; + ClayLayoutSolverContext& operator=(const ClayLayoutSolverContext&) = delete; + ClayLayoutSolverContext(ClayLayoutSolverContext&&) noexcept; + ClayLayoutSolverContext& operator=(ClayLayoutSolverContext&&) noexcept; + + private: + friend class ClayLayoutSolver; + + struct Impl; + std::unique_ptr m_impl; + }; + + struct ClayLayoutSettings + { + CLayoutGroup::EDirection direction = CLayoutGroup::EDirection::HORIZONTAL; + float spacing = 0.0f; + OvMaths::FVector4 padding = OvMaths::FVector4::Zero; + CLayoutGroup::EHorizontalAlignment horizontalAlignment = CLayoutGroup::EHorizontalAlignment::CENTER; + CLayoutGroup::EVerticalAlignment verticalAlignment = CLayoutGroup::EVerticalAlignment::CENTER; + bool controlChildrenWidth = false; + bool controlChildrenHeight = false; + bool forceExpandWidth = false; + bool forceExpandHeight = false; + OvMaths::FVector2 containerSize = OvMaths::FVector2::Zero; + OvMaths::FVector2 pivot = OvMaths::FVector2::Zero; + }; + + struct ClayLayoutChildInput + { + ECS::Actor* actor = nullptr; + OvMaths::FVector2 preferredSize = OvMaths::FVector2::Zero; + }; + + struct ClayLayoutChildResult + { + ECS::Actor* actor = nullptr; + OvMaths::FVector2 offset = OvMaths::FVector2::Zero; + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + bool valid = false; + }; + + struct ClayLayoutResult + { + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + std::vector children; + }; + + struct ClayLayoutMeasurement + { + ClayLayoutSettings settings; + OvMaths::FVector2 preferredSize = OvMaths::FVector2::Zero; + bool valid = false; + }; + + struct ClayLayoutSolution + { + ClayLayoutSettings settings; + OvMaths::FVector2 preferredSize = OvMaths::FVector2::Zero; + ClayLayoutResult result; + bool valid = false; + }; + + class ClayLayoutSolver + { + public: + static ClayLayoutMeasurement Measure( + ClayLayoutSolverContext& p_context, + const ClayLayoutSettings& p_settings, + const std::vector& p_children + ); + + static ClayLayoutSolution SolveLayout( + ClayLayoutSolverContext& p_context, + const ClayLayoutMeasurement& p_measurement, + const std::vector& p_children + ); + + static ClayLayoutResult Postprocess(const ClayLayoutSolution& p_solution); + + static ClayLayoutResult Solve( + ClayLayoutSolverContext& p_context, + const ClayLayoutSettings& p_settings, + const std::vector& p_children + ); + }; +} diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/TextLayoutEngine.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/TextLayoutEngine.h new file mode 100644 index 000000000..e72430089 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/TextLayoutEngine.h @@ -0,0 +1,68 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include + +#include +#include + +namespace OvCore::ECS::Components::UI +{ + /** + * Resolves text wrapping, alignment and bounds without creating render geometry. + */ + class TextLayoutEngine + { + public: + enum class EHorizontalAlignment + { + LEFT, + CENTER, + RIGHT + }; + + enum class EVerticalAlignment + { + TOP, + CENTER, + BOTTOM + }; + + struct Input + { + std::string_view text; + OvRendering::Resources::Font* font = nullptr; + float fontSize = 1.0f; + OvMaths::FVector2 uiSize = OvMaths::FVector2::Zero; + EHorizontalAlignment horizontalAlignment = EHorizontalAlignment::LEFT; + EVerticalAlignment verticalAlignment = EVerticalAlignment::TOP; + }; + + struct Glyph + { + float left = 0.0f; + float right = 0.0f; + float bottom = 0.0f; + float top = 0.0f; + float uMin = 0.0f; + float uMax = 0.0f; + float vMin = 0.0f; + float vMax = 0.0f; + }; + + struct Output + { + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + OvMaths::FVector2 contentSize = OvMaths::FVector2::Zero; + std::vector glyphs; + }; + + static Output Layout(const Input& p_input); + }; +} diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/TextMeshBuilder.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/TextMeshBuilder.h new file mode 100644 index 000000000..577ad85d2 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/TextMeshBuilder.h @@ -0,0 +1,35 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +#include +#include + +namespace OvCore::ECS::Components::UI +{ + /** + * Builds text layout geometry from resolved font data. + */ + class TextMeshBuilder + { + public: + using EHorizontalAlignment = TextLayoutEngine::EHorizontalAlignment; + using EVerticalAlignment = TextLayoutEngine::EVerticalAlignment; + using Input = TextLayoutEngine::Input; + + struct Output + { + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + std::unique_ptr mesh; + }; + + static Output Build(const Input& p_input); + static Output Build(const TextLayoutEngine::Output& p_layout); + }; +} diff --git a/Sources/OvCore/include/OvCore/ECS/Components/UI/UITransformResolver.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/UITransformResolver.h new file mode 100644 index 000000000..98bd33c2f --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/UITransformResolver.h @@ -0,0 +1,62 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI::UITransformResolver +{ + struct LayoutData + { + OvMaths::FVector2 offset = OvMaths::FVector2::Zero; + OvMaths::FVector2 directSize = OvMaths::FVector2::Zero; + bool hasDirectWidth = false; + bool hasDirectHeight = false; + }; + + CTransform::EUIAnchorPreset ToAnchorPreset(int p_value); + OvMaths::FVector2 GetAnchorRatio(CTransform::EUIAnchorPreset p_anchorPreset); + bool IsHorizontalPositionEditable(CTransform::EUIAnchorPreset p_anchorPreset); + bool IsVerticalPositionEditable(CTransform::EUIAnchorPreset p_anchorPreset); + + ECS::Actor* FindCanvasOwner(ECS::Actor& p_owner); + const ECS::Actor* FindCanvasOwner(const ECS::Actor& p_owner); + const ECS::Actor* FindActiveCanvasOwner(const ECS::Actor& p_owner); + bool HasActiveUIData(const ECS::Actor& p_owner); + + bool IsDrivenByLayout(const ECS::Actor& p_owner); + LayoutData ResolveLayoutData(const ECS::Actor& p_owner); + + OvMaths::FVector2 GetEffectiveSize( + const CTransform& p_transform, + const OvMaths::FVector2& p_elementSize + ); + + OvMaths::FVector2 GetAnchoredPosition( + const CTransform& p_transform, + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset + ); + + OvMaths::FMatrix4 GetMatrix( + const CTransform& p_transform, + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset, + const OvMaths::FVector2& p_elementSize + ); + + OvMaths::FMatrix4 GetMatrixWithEffectiveSize( + const CTransform& p_transform, + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset, + const OvMaths::FVector2& p_effectiveSize + ); +} diff --git a/Sources/OvCore/include/OvCore/Helpers/GUIHelpers.h b/Sources/OvCore/include/OvCore/Helpers/GUIHelpers.h index e9d701d18..c24e6a0d3 100644 --- a/Sources/OvCore/include/OvCore/Helpers/GUIHelpers.h +++ b/Sources/OvCore/include/OvCore/Helpers/GUIHelpers.h @@ -61,6 +61,7 @@ namespace OvCore::Helpers using FileItemBuilderCallback = std::function, bool, bool)>; using OpenProviderCallback = std::function; using PickerProviderCallback = std::function; + using PickerCloseProviderCallback = std::function; using PickerSearchTextProviderCallback = std::function; using IconProviderCallback = std::function; using ActorSelectionProviderCallback = std::function; @@ -86,6 +87,8 @@ namespace OvCore::Helpers static void SetPickerProvider(PickerProviderCallback p_provider); static void OpenPicker(PickerItemList p_items, std::string p_title); + static void SetPickerCloseProvider(PickerCloseProviderCallback p_provider); + static void ClosePicker(); static void SetPickerSearchTextProvider(PickerSearchTextProviderCallback p_provider); static std::string GetPickerSearchText(); 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..4ab9ff612 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 @@ -23,6 +26,8 @@ namespace OvCore::Rendering { + namespace UIRenderingUtils { class UIFrameResolver; } + /** * Extension of the CompositeRenderer adding support for the scene system (parsing/drawing entities) */ @@ -39,7 +44,7 @@ namespace OvCore::Rendering struct DrawOrder { const int order; - const uintptr_t materialKey; + const OvRendering::Data::Material* materialKey; const float distance; /** @@ -86,11 +91,16 @@ 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; + const UIRenderingUtils::UIFrameResolver* uiFrameResolver = nullptr; }; /** @@ -109,6 +119,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..90b114585 --- /dev/null +++ b/Sources/OvCore/include/OvCore/Rendering/UIRenderingUtils.h @@ -0,0 +1,211 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace OvCore::ECS { class Actor; } +namespace OvCore::ECS::Components::UI { class CCanvas; } + +namespace OvCore::Rendering::UIRenderingUtils +{ + struct ResolvedUICanvas + { + const OvCore::ECS::Actor* actor = nullptr; + const OvCore::ECS::Components::UI::CCanvas* canvas = nullptr; + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + OvMaths::FMatrix4 matrix = OvMaths::FMatrix4::Identity; + OvMaths::FMatrix4 modelMatrix = OvMaths::FMatrix4::Identity; + float canvasScale = 1.0f; + float worldScale = 1.0f; + float unitsScale = 1.0f; + bool screenSpace = false; + }; + + struct ResolvedUIElement + { + const OvCore::ECS::Actor* actor = nullptr; + const OvCore::ECS::Actor* canvasActor = nullptr; + const OvCore::ECS::Components::UI::CCanvas* canvas = nullptr; + OvMaths::FVector2 canvasSize = OvMaths::FVector2::Zero; + OvMaths::FVector2 layoutOffset = OvMaths::FVector2::Zero; + OvMaths::FVector2 elementSize = OvMaths::FVector2::Zero; + OvMaths::FVector2 effectiveSize = OvMaths::FVector2::Zero; + OvMaths::FMatrix4 canvasMatrix = OvMaths::FMatrix4::Identity; + OvMaths::FMatrix4 localMatrix = OvMaths::FMatrix4::Identity; + OvMaths::FMatrix4 frameMatrix = OvMaths::FMatrix4::Identity; + OvMaths::FMatrix4 modelMatrix = OvMaths::FMatrix4::Identity; + float canvasScale = 1.0f; + float worldScale = 1.0f; + float unitsScale = 1.0f; + bool screenSpace = false; + }; + + class UIFrameResolver + { + public: + UIFrameResolver( + const OvMaths::FVector2& p_renderSize = { 1.0f, 1.0f }, + bool p_screenSpace = true + ); + + const OvMaths::FVector2& GetRenderSize() const; + bool IsScreenSpace() const; + + OvMaths::FMatrix4 CreateProjectionMatrix( + float p_near = -1.0f, + float p_far = 1.0f + ) const; + + OvMaths::FVector2 GetElementSize(const OvCore::ECS::Actor& p_actor) const; + + bool ResolveCanvas( + const OvCore::ECS::Actor& p_actor, + ResolvedUICanvas& p_outCanvas + ) const; + + bool ResolveElement( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_elementSize, + ResolvedUIElement& p_outElement + ) const; + + bool ResolveElement( + const OvCore::ECS::Actor& p_actor, + ResolvedUIElement& p_outElement + ) const; + + private: + struct ElementKey + { + const OvCore::ECS::Actor* actor = nullptr; + float width = 0.0f; + float height = 0.0f; + + bool operator==(const ElementKey& p_other) const; + }; + + struct ElementKeyHash + { + std::size_t operator()(const ElementKey& p_key) const; + }; + + struct CachedLayoutData + { + OvMaths::FVector2 offset = OvMaths::FVector2::Zero; + OvMaths::FVector2 directSize = OvMaths::FVector2::Zero; + bool hasDirectWidth = false; + bool hasDirectHeight = false; + bool drivenByLayout = false; + }; + + bool ResolveCanvasUncached( + const OvCore::ECS::Actor& p_actor, + ResolvedUICanvas& p_outCanvas + ) const; + + bool ResolveElementUncached( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_elementSize, + ResolvedUIElement& p_outElement + ) const; + + bool HasActiveUIData(const OvCore::ECS::Actor& p_actor) const; + const OvCore::ECS::Actor* FindCanvasOwner(const OvCore::ECS::Actor& p_actor) const; + CachedLayoutData GetLayoutData(const OvCore::ECS::Actor& p_actor) const; + + private: + OvMaths::FVector2 m_renderSize = { 1.0f, 1.0f }; + bool m_screenSpace = true; + mutable std::unordered_map m_activeUIDataCache; + mutable std::unordered_map m_canvasOwnerCache; + mutable std::unordered_map m_layoutDataCache; + mutable std::unordered_map m_elementSizeCache; + mutable std::unordered_map> m_canvasCache; + mutable std::unordered_map, ElementKeyHash> m_elementCache; + }; + + OvMaths::FVector2 ClampCanvasSize(const OvMaths::FVector2& p_canvasSize); + + OvMaths::FMatrix4 CreateUIProjectionMatrix( + const OvMaths::FVector2& p_renderSize, + float p_near = -1.0f, + float p_far = 1.0f + ); + + 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); + + OvCore::ECS::Actor* FindCanvasOwner(OvCore::ECS::Actor& p_owner); + const OvCore::ECS::Actor* FindCanvasOwner(const OvCore::ECS::Actor& p_owner); + + OvMaths::FMatrix4 GetCanvasMatrix( + const OvCore::ECS::Actor& p_owner, + bool p_screenSpace + ); + + OvMaths::FVector2 GetCanvasSize( + const OvCore::ECS::Actor& p_owner, + const OvMaths::FVector2& p_renderSize + ); + + OvMaths::FVector2 GetElementSize( + 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 + ); + + OvMaths::FVector3 TransformUIPoint( + const OvMaths::FMatrix4& p_matrix, + const OvMaths::FVector2& p_point + ); + + OvMaths::FVector3 TransformUIElementPivot(const ResolvedUIElement& p_element); + + bool ResolveUICanvas( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_renderSize, + bool p_screenSpace, + ResolvedUICanvas& p_outCanvas + ); + + bool ResolveUIElement( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_renderSize, + bool p_screenSpace, + const OvMaths::FVector2& p_elementSize, + ResolvedUIElement& p_outElement + ); + + bool ResolveUIElement( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_renderSize, + bool p_screenSpace, + ResolvedUIElement& p_outElement + ); +} 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/include/OvCore/ResourceManagement/UIResourceRegistry.h b/Sources/OvCore/include/OvCore/ResourceManagement/UIResourceRegistry.h new file mode 100644 index 000000000..357ef61e7 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ResourceManagement/UIResourceRegistry.h @@ -0,0 +1,40 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +namespace OvCore::ResourceManagement +{ + /** + * Stores host-provided default resources used by UI components. + */ + class UIResourceRegistry + { + public: + struct Definition + { + std::string imageMaterialPath; + std::string textMaterialPath; + std::string defaultFontPath; + }; + + /** + * Provides the UI resource definition used by UI components. + * @param p_definition + */ + void ProvideDefinition(Definition p_definition); + + /** + * Returns the current UI resource definition. + */ + const Definition& GetDefinition() const; + + private: + Definition m_definition; + }; +} 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..ef2089ae0 100644 --- a/Sources/OvCore/src/OvCore/ECS/Actor.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Actor.cpp @@ -25,6 +25,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include @@ -606,6 +612,12 @@ 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(); if (component) { diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CTransform.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CTransform.cpp index 79ddff68b..3bb80b6c4 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CTransform.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CTransform.cpp @@ -4,7 +4,48 @@ * @licence: MIT */ +#include +#include +#include +#include + +#include + #include +#include +#include +#include +#include +#include +#include + +namespace +{ + 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::CTransform::CTransform(ECS::Actor& p_owner, OvMaths::FVector3 p_localPosition, OvMaths::FQuaternion p_localRotation, OvMaths::FVector3 p_localScale) : AComponent(p_owner) @@ -157,11 +198,163 @@ OvMaths::FVector3 OvCore::ECS::Components::CTransform::GetLocalRight() const return m_transform.GetLocalRight(); } +OvCore::ECS::Components::CTransform::EUIAnchorPreset OvCore::ECS::Components::CTransform::ToUIAnchorPreset(int p_value) +{ + return OvCore::ECS::Components::UI::UITransformResolver::ToAnchorPreset(p_value); +} + +bool OvCore::ECS::Components::CTransform::IsHorizontalUIPositionEditable(EUIAnchorPreset p_anchorPreset) +{ + return OvCore::ECS::Components::UI::UITransformResolver::IsHorizontalPositionEditable(p_anchorPreset); +} + +bool OvCore::ECS::Components::CTransform::IsVerticalUIPositionEditable(EUIAnchorPreset p_anchorPreset) +{ + return OvCore::ECS::Components::UI::UITransformResolver::IsVerticalPositionEditable(p_anchorPreset); +} + +OvCore::ECS::Components::CTransform::UIData& OvCore::ECS::Components::CTransform::GetOrCreateUIData() +{ + if (!m_uiData) + { + m_uiData = UIData{}; + } + + return m_uiData.value(); +} + +const OvCore::ECS::Components::CTransform::UIData& OvCore::ECS::Components::CTransform::GetUIDataOrDefault() const +{ + static const UIData kDefaultUIData{}; + return m_uiData ? m_uiData.value() : kDefaultUIData; +} + +void OvCore::ECS::Components::CTransform::EnableUIData() +{ + GetOrCreateUIData(); +} + +void OvCore::ECS::Components::CTransform::DisableUIData() +{ + m_uiData.reset(); +} + +bool OvCore::ECS::Components::CTransform::HasUIData() const +{ + return m_uiData.has_value(); +} + +bool OvCore::ECS::Components::CTransform::HasActiveUIData() const +{ + return OvCore::ECS::Components::UI::UITransformResolver::HasActiveUIData(owner); +} + +const std::optional& OvCore::ECS::Components::CTransform::GetUIData() const +{ + return m_uiData; +} + +void OvCore::ECS::Components::CTransform::SetUIPosition(const OvMaths::FVector2& p_position) +{ + auto& uiData = GetOrCreateUIData(); + uiData.position.x = KeepFinite(p_position.x, uiData.position.x); + uiData.position.y = KeepFinite(p_position.y, uiData.position.y); +} + +const OvMaths::FVector2& OvCore::ECS::Components::CTransform::GetUIPosition() const +{ + return GetUIDataOrDefault().position; +} + +void OvCore::ECS::Components::CTransform::SetUIRotation(float p_rotation) +{ + const auto rotation = KeepFinite(p_rotation, GetUIRotation()); + auto eulerRotation = OvMaths::FQuaternion::EulerAngles(GetLocalRotation()); + eulerRotation.z = rotation; + SetLocalRotation(OvMaths::FQuaternion(eulerRotation)); +} + +float OvCore::ECS::Components::CTransform::GetUIRotation() const +{ + const auto eulerRotation = OvMaths::FQuaternion::EulerAngles(GetLocalRotation()); + return KeepFinite(eulerRotation.z, 0.0f); +} + +void OvCore::ECS::Components::CTransform::SetUIScale(const OvMaths::FVector2& p_scale) +{ + auto localScale = GetLocalScale(); + localScale.x = ClampScaleAxis(p_scale.x, localScale.x); + localScale.y = ClampScaleAxis(p_scale.y, localScale.y); + SetLocalScale(localScale); +} + +OvMaths::FVector2 OvCore::ECS::Components::CTransform::GetUIScale() const +{ + const auto& localScale = GetLocalScale(); + return { + ClampScaleAxis(localScale.x, kMinimumScale), + ClampScaleAxis(localScale.y, kMinimumScale) + }; +} + +void OvCore::ECS::Components::CTransform::SetUISize(const OvMaths::FVector2& p_size) +{ + auto& uiData = GetOrCreateUIData(); + uiData.size.x = std::isfinite(p_size.x) ? std::max(p_size.x, kMinimumSize) : uiData.size.x; + uiData.size.y = std::isfinite(p_size.y) ? std::max(p_size.y, kMinimumSize) : uiData.size.y; +} + +const OvMaths::FVector2& OvCore::ECS::Components::CTransform::GetUISize() const +{ + return GetUIDataOrDefault().size; +} + +void OvCore::ECS::Components::CTransform::SetUIPivot(const OvMaths::FVector2& p_pivot) +{ + auto& uiData = GetOrCreateUIData(); + uiData.pivot.x = ClampPivotAxis(p_pivot.x, uiData.pivot.x); + uiData.pivot.y = ClampPivotAxis(p_pivot.y, uiData.pivot.y); +} + +const OvMaths::FVector2& OvCore::ECS::Components::CTransform::GetUIPivot() const +{ + return GetUIDataOrDefault().pivot; +} + +void OvCore::ECS::Components::CTransform::SetUIAnchorPreset(EUIAnchorPreset p_anchorPreset) +{ + GetOrCreateUIData().anchorPreset = ToUIAnchorPreset(static_cast(p_anchorPreset)); +} + +OvCore::ECS::Components::CTransform::EUIAnchorPreset OvCore::ECS::Components::CTransform::GetUIAnchorPreset() const +{ + return GetUIDataOrDefault().anchorPreset; +} + +bool OvCore::ECS::Components::CTransform::IsHorizontalUIPositionEditable() const +{ + return IsHorizontalUIPositionEditable(GetUIAnchorPreset()); +} + +bool OvCore::ECS::Components::CTransform::IsVerticalUIPositionEditable() const +{ + return IsVerticalUIPositionEditable(GetUIAnchorPreset()); +} + void OvCore::ECS::Components::CTransform::OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) { OvCore::Helpers::Serializer::SerializeVec3(p_doc, p_node, "position", GetLocalPosition()); OvCore::Helpers::Serializer::SerializeQuat(p_doc, p_node, "rotation", GetLocalRotation()); OvCore::Helpers::Serializer::SerializeVec3(p_doc, p_node, "scale", GetLocalScale()); + + if (HasUIData()) + { + OvCore::Helpers::Serializer::SerializeBoolean(p_doc, p_node, "ui_enabled", true); + OvCore::Helpers::Serializer::SerializeVec2(p_doc, p_node, "ui_position", GetUIPosition()); + OvCore::Helpers::Serializer::SerializeVec2(p_doc, p_node, "ui_size", GetUISize()); + OvCore::Helpers::Serializer::SerializeVec2(p_doc, p_node, "ui_pivot", GetUIPivot()); + OvCore::Helpers::Serializer::SerializeInt(p_doc, p_node, "ui_anchor_preset", static_cast(GetUIAnchorPreset())); + } } void OvCore::ECS::Components::CTransform::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode * p_node) @@ -172,21 +365,176 @@ void OvCore::ECS::Components::CTransform::OnDeserialize(tinyxml2::XMLDocument & OvCore::Helpers::Serializer::DeserializeQuat(p_doc, p_node, "rotation"), OvCore::Helpers::Serializer::DeserializeVec3(p_doc, p_node, "scale") ); + + bool uiEnabled = true; + if (p_node->FirstChildElement("ui_enabled")) + { + OvCore::Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "ui_enabled", uiEnabled); + if (uiEnabled) + { + EnableUIData(); + } + else + { + DisableUIData(); + } + } + + if (uiEnabled && p_node->FirstChildElement("ui_position")) + { + auto position = GetUIPosition(); + OvCore::Helpers::Serializer::DeserializeVec2(p_doc, p_node, "ui_position", position); + SetUIPosition(position); + } + + if (uiEnabled && p_node->FirstChildElement("ui_size")) + { + auto size = GetUISize(); + OvCore::Helpers::Serializer::DeserializeVec2(p_doc, p_node, "ui_size", size); + SetUISize(size); + } + + if (uiEnabled && p_node->FirstChildElement("ui_pivot")) + { + auto pivot = GetUIPivot(); + OvCore::Helpers::Serializer::DeserializeVec2(p_doc, p_node, "ui_pivot", pivot); + SetUIPivot(pivot); + } + + if (uiEnabled && p_node->FirstChildElement("ui_anchor_preset")) + { + auto anchorPreset = static_cast(GetUIAnchorPreset()); + OvCore::Helpers::Serializer::DeserializeInt(p_doc, p_node, "ui_anchor_preset", anchorPreset); + SetUIAnchorPreset(ToUIAnchorPreset(anchorPreset)); + } } void OvCore::ECS::Components::CTransform::OnInspector(OvUI::Internal::WidgetContainer& p_root) { - auto getRotation = [this] - { - return OvMaths::FQuaternion::EulerAngles(GetLocalRotation()); - }; - - auto setRotation = [this](OvMaths::FVector3 result) + if (HasActiveUIData()) { - 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); - 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); + OvCore::Helpers::GUIDrawer::CreateTitle(p_root, "Anchored Position (px)"); + auto& anchoredPosition = p_root.CreateWidget>( + OvCore::Helpers::GUIDrawer::GetDataType(), + OvCore::Helpers::GUIDrawer::_MIN_FLOAT, + OvCore::Helpers::GUIDrawer::_MAX_FLOAT, + 0.0f, + 1.0f, + "", + OvCore::Helpers::GUIDrawer::GetFormat() + ); + auto& anchoredPositionDispatcher = anchoredPosition.AddPlugin>>(); + anchoredPositionDispatcher.RegisterGatherer([this]() + { + const auto value = GetUIPosition(); + return std::array{ value.x, value.y }; + }); + anchoredPositionDispatcher.RegisterProvider([this](std::array p_value) + { + auto position = GetUIPosition(); + const auto anchorPreset = GetUIAnchorPreset(); + + if (IsHorizontalUIPositionEditable(anchorPreset)) + { + position.x = p_value[0]; + } + + if (IsVerticalUIPositionEditable(anchorPreset)) + { + position.y = p_value[1]; + } + + SetUIPosition(position); + }); + + const auto updateAnchoredPositionEditability = [this, anchoredPositionWidget = &anchoredPosition]() + { + const auto anchorPreset = GetUIAnchorPreset(); + const bool isHorizontalEditable = IsHorizontalUIPositionEditable(anchorPreset); + const bool isVerticalEditable = IsVerticalUIPositionEditable(anchorPreset); + + anchoredPositionWidget->disabled = OvCore::ECS::Components::UI::UITransformResolver::IsDrivenByLayout(owner) || (!isHorizontalEditable && !isVerticalEditable); + }; + updateAnchoredPositionEditability(); + + OvCore::Helpers::GUIDrawer::DrawVec2( + p_root, + "Size", + [this]() { return GetUISize(); }, + [this](OvMaths::FVector2 p_value) { SetUISize(p_value); }, + 1.0f, + kMinimumSize + ); + + OvCore::Helpers::GUIDrawer::DrawVec2( + p_root, + "Pivot", + [this]() { return GetUIPivot(); }, + [this](OvMaths::FVector2 p_value) { SetUIPivot(p_value); }, + 0.01f, + kMinimumPivot, + kMaximumPivot + ); + + OvCore::Helpers::GUIDrawer::DrawScalar( + p_root, + "Rotation", + [this]() { return GetUIRotation(); }, + [this](float p_value) { SetUIRotation(p_value); }, + 0.05f, + OvCore::Helpers::GUIDrawer::_MIN_FLOAT, + OvCore::Helpers::GUIDrawer::_MAX_FLOAT + ); + + OvCore::Helpers::GUIDrawer::DrawVec2( + p_root, + "Scale", + [this]() { return GetUIScale(); }, + [this](OvMaths::FVector2 p_value) { SetUIScale(p_value); }, + 0.05f, + kMinimumScale + ); + + OvCore::Helpers::GUIDrawer::CreateTitle(p_root, "Anchor Preset"); + auto& anchorPreset = p_root.CreateWidget(static_cast(GetUIAnchorPreset())); + anchorPreset.disabled = OvCore::ECS::Components::UI::UITransformResolver::IsDrivenByLayout(owner); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::TOP_LEFT), "Top Left"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::TOP_CENTER), "Top Center"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::TOP_RIGHT), "Top Right"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::MIDDLE_LEFT), "Middle Left"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::CENTER), "Center"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::MIDDLE_RIGHT), "Middle Right"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::BOTTOM_LEFT), "Bottom Left"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::BOTTOM_CENTER), "Bottom Center"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::BOTTOM_RIGHT), "Bottom Right"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::HORIZONTAL_STRETCH_TOP), "Horizontal Stretch Top"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::HORIZONTAL_STRETCH_MIDDLE), "Horizontal Stretch Middle"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::HORIZONTAL_STRETCH_BOTTOM), "Horizontal Stretch Bottom"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::VERTICAL_STRETCH_LEFT), "Vertical Stretch Left"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::VERTICAL_STRETCH_CENTER), "Vertical Stretch Center"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::VERTICAL_STRETCH_RIGHT), "Vertical Stretch Right"); + anchorPreset.choices.emplace(static_cast(EUIAnchorPreset::STRETCH_BOTH), "Stretch Both"); + + anchorPreset.ValueChangedEvent += [this, updateAnchoredPositionEditability](int p_choice) + { + SetUIAnchorPreset(ToUIAnchorPreset(p_choice)); + updateAnchoredPositionEditability(); + }; + } + else + { + auto getRotation = [this] + { + return OvMaths::FQuaternion::EulerAngles(GetLocalRotation()); + }; + + auto setRotation = [this](OvMaths::FVector3 result) + { + 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); + 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..cb5c70c49 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CCanvas.cpp @@ -0,0 +1,298 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#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; + } + } + +} + +OvCore::ECS::Components::UI::CCanvas::CCanvas(ECS::Actor& p_owner) : +AComponent(p_owner) +{ + owner.transform.EnableUIData(); +} + +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& screenMatchModeTitle = p_root.CreateWidget("Screen Match Mode", OVUI_STYLE(InspectorTitle)); + auto& screenMatchMode = p_root.CreateWidget(static_cast(GetScreenMatchMode())); + screenMatchMode.choices.emplace(static_cast(EScreenMatchMode::MATCH_WIDTH_OR_HEIGHT), "Match Width Or Height"); + screenMatchMode.choices.emplace(static_cast(EScreenMatchMode::EXPAND), "Expand"); + screenMatchMode.choices.emplace(static_cast(EScreenMatchMode::SHRINK), "Shrink"); + auto& screenMatchModeDispatcher = screenMatchMode.AddPlugin>(); + screenMatchModeDispatcher.RegisterGatherer([this]() { return static_cast(GetScreenMatchMode()); }); + screenMatchModeDispatcher.RegisterProvider([this](int p_choice) { SetScreenMatchMode(ToScreenMatchMode(p_choice)); }); + + auto& matchWidthOrHeightTitle = p_root.CreateWidget("Match Width Or Height", OVUI_STYLE(InspectorTitle)); + auto& matchWidthOrHeight = p_root.CreateWidget>( + OvCore::Helpers::GUIDrawer::GetDataType(), + kMinimumMatchWidthOrHeight, + kMaximumMatchWidthOrHeight, + 0.0f, + 0.01f, + "", + OvCore::Helpers::GUIDrawer::GetFormat() + ); + auto& matchWidthOrHeightDispatcher = matchWidthOrHeight.AddPlugin>(); + matchWidthOrHeightDispatcher.RegisterGatherer([this]() { return GetMatchWidthOrHeight(); }); + matchWidthOrHeightDispatcher.RegisterProvider([this](float p_value) { SetMatchWidthOrHeight(p_value); }); + + const auto updateScaleWithScreenSettingsVisibility = + [this, + screenMatchModeTitleWidget = &screenMatchModeTitle, + screenMatchModeWidget = &screenMatchMode, + matchWidthOrHeightTitleWidget = &matchWidthOrHeightTitle, + matchWidthOrHeightWidget = &matchWidthOrHeight]() + { + const bool usesScreenSize = GetScalerMode() == EScalerMode::SCALE_WITH_SCREEN_SIZE; + screenMatchModeTitleWidget->enabled = usesScreenSize; + screenMatchModeWidget->enabled = usesScreenSize; + + const bool usesMatchWidthOrHeight = usesScreenSize && GetScreenMatchMode() == EScreenMatchMode::MATCH_WIDTH_OR_HEIGHT; + matchWidthOrHeightTitleWidget->enabled = usesMatchWidthOrHeight; + matchWidthOrHeightWidget->enabled = usesMatchWidthOrHeight; + }; + + scalerMode.ValueChangedEvent += [this, updateScaleWithScreenSettingsVisibility](int p_choice) + { + SetScalerMode(ToScalerMode(p_choice)); + updateScaleWithScreenSettingsVisibility(); + }; + + screenMatchMode.ValueChangedEvent += [this, updateScaleWithScreenSettingsVisibility](int p_choice) + { + SetScreenMatchMode(ToScreenMatchMode(p_choice)); + updateScaleWithScreenSettingsVisibility(); + }; + + updateScaleWithScreenSettingsVisibility(); +} 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..142a6eb86 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CImage.cpp @@ -0,0 +1,331 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr const char* kTextureUniform = "u_Image"; + constexpr const char* kTintUniform = "u_Tint"; + + OvMaths::FVector2 GetDefaultImageSize() + { + return { 100.0f, 100.0f }; + } + + 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 }; + } + + bool IsRegisteredTexture(const OvRendering::Resources::Texture* p_texture) + { + if (!p_texture) + { + return false; + } + + for (const auto& [_, texture] : OvCore::Global::ServiceLocator::Get().GetResources()) + { + if (texture == p_texture) + { + return true; + } + } + + return false; + } +} + +OvCore::ECS::Components::UI::CImage::CImage(ECS::Actor& p_owner) : +AComponent(p_owner) +{ + m_textureChangedEvent += [this] + { + m_textureReferenceDirty = m_texture != nullptr; + m_materialTextureDirty = true; + UpdateIntrinsicSize(); + RebuildMesh(); + }; + + owner.transform.EnableUIData(); + UpdateIntrinsicSize(); + 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) +{ + if (m_texture == p_texture) + { + return; + } + + m_texture = p_texture; + m_textureReferenceDirty = p_texture != nullptr; + m_materialTextureDirty = true; + UpdateIntrinsicSize(); + RebuildMesh(); +} + +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) +{ + owner.transform.SetUISize(p_size); +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::CImage::GetSize() const +{ + const auto& transformSize = owner.transform.GetUISize(); + const auto intrinsicSize = GetIntrinsicSize(); + + return { + transformSize.x > 0.0f ? transformSize.x : intrinsicSize.x, + transformSize.y > 0.0f ? transformSize.y : intrinsicSize.y + }; +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::CImage::GetIntrinsicSize() const +{ + return m_intrinsicSize; +} + +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); + m_materialTintDirty = true; +} + +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) +{ + ValidateTextureReference(); + Helpers::Serializer::SerializeTexture(p_doc, p_node, "texture", m_texture); + 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("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) +{ + ValidateTextureReference(); + Helpers::GUIDrawer::DrawTexture(p_root, "Texture", m_texture, &m_textureChangedEvent); + + 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 auto size = GetIntrinsicSize(); + + const float halfWidth = size.x * 0.5f; + const float halfHeight = 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::ValidateTextureReference() +{ + m_textureReferenceDirty = false; + + if (m_texture && !IsRegisteredTexture(m_texture)) + { + m_texture = nullptr; + m_materialTextureDirty = true; + UpdateIntrinsicSize(); + RebuildMesh(); + } +} + +void OvCore::ECS::Components::UI::CImage::UpdateIntrinsicSize() +{ + const auto defaultSize = GetDefaultImageSize(); + m_intrinsicSize = defaultSize; + + if (!m_texture || !IsRegisteredTexture(m_texture)) + { + return; + } + + const auto& textureDesc = m_texture->GetTexture().GetDesc(); + m_intrinsicSize = { + textureDesc.width > 0 ? static_cast(textureDesc.width) : defaultSize.x, + textureDesc.height > 0 ? static_cast(textureDesc.height) : defaultSize.y + }; +} + +void OvCore::ECS::Components::UI::CImage::RefreshMaterial() +{ + if (!m_material) + { + m_material = std::make_unique(); + m_materialStateDirty = true; + m_materialTextureDirty = true; + m_materialTintDirty = true; + } + + if (m_textureReferenceDirty) + { + ValidateTextureReference(); + } + + if (m_materialTexture != m_texture) + { + m_materialTextureDirty = true; + } + + if (!m_materialStateDirty && !m_materialTextureDirty && !m_materialTintDirty) + { + return; + } + + if (m_materialStateDirty) + { + const auto& imageMaterialPath = Global::ServiceLocator::Get().GetDefinition().imageMaterialPath; + auto* defaultMaterial = imageMaterialPath.empty() ? + nullptr : + Global::ServiceLocator::Get().GetResource(imageMaterialPath); + + if (!defaultMaterial || !defaultMaterial->HasShader()) + { + if (m_material->HasShader()) + { + m_material->SetShader(nullptr); + } + + m_materialTextureDirty = true; + m_materialTintDirty = true; + return; + } + + if (m_material->GetShader() != defaultMaterial->GetShader()) + { + m_material->SetShader(defaultMaterial->GetShader()); + m_materialTextureDirty = true; + m_materialTintDirty = true; + } + + 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_materialStateDirty = false; + } + + if (!m_material->IsValid()) + { + return; + } + + if (m_materialTextureDirty) + { + m_material->TrySetProperty(kTextureUniform, m_texture); + m_materialTexture = m_texture; + m_materialTextureDirty = false; + } + + if (m_materialTintDirty) + { + m_material->TrySetProperty(kTintUniform, m_tint); + m_materialTintDirty = false; + } +} 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..8ec2d6d84 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CLayoutGroup.cpp @@ -0,0 +1,638 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + constexpr float kMinimumSpacing = 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()); + + float ClampSpacing(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? std::clamp(p_value, kMinimumSpacing, kMaximumSpacing) : p_fallback; + } + + float ClampPadding(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? std::clamp(p_value, kMinimumPadding, kMaximumPadding) : p_fallback; + } + + 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) + { + OvMaths::FVector2 elementSize = OvMaths::FVector2::Zero; + bool hasElementSize = false; + + if (const auto* image = p_child.GetComponent(); image) + { + elementSize = image->GetSize(); + hasElementSize = true; + } + else if (const auto* text = p_child.GetComponent(); text) + { + elementSize = text->GetSize(); + hasElementSize = true; + } + else if (const auto* layout = p_child.GetComponent(); layout) + { + elementSize = layout->GetComputedSize(); + hasElementSize = true; + } + + if (OvCore::ECS::Components::UI::UITransformResolver::HasActiveUIData(p_child)) + { + const auto size = OvCore::ECS::Components::UI::UITransformResolver::GetEffectiveSize(p_child.transform, elementSize); + if (size.x > 0.0f && size.y > 0.0f) + { + return size; + } + } + + if (hasElementSize && elementSize.x > 0.0f && elementSize.y > 0.0f) + { + return elementSize; + } + + return std::nullopt; + } + + OvCore::ECS::Components::UI::ClayLayoutSettings CreateLayoutSettings( + const OvCore::ECS::Components::UI::CLayoutGroup& p_layout, + const OvCore::ECS::Actor& p_owner + ) + { + return { + .direction = p_layout.GetDirection(), + .spacing = p_layout.GetSpacing(), + .padding = p_layout.GetPadding(), + .horizontalAlignment = p_layout.GetHorizontalAlignment(), + .verticalAlignment = p_layout.GetVerticalAlignment(), + .controlChildrenWidth = p_layout.GetControlChildrenWidth(), + .controlChildrenHeight = p_layout.GetControlChildrenHeight(), + .forceExpandWidth = p_layout.GetForceExpandWidth(), + .forceExpandHeight = p_layout.GetForceExpandHeight(), + .containerSize = p_owner.transform.GetUISize(), + .pivot = p_owner.transform.GetUIPivot() + }; + } +} + +OvCore::ECS::Components::UI::CLayoutGroup::CLayoutGroup(ECS::Actor& p_owner) : +AComponent(p_owner) +{ + m_layoutSolverContext = std::make_unique(); + owner.transform.EnableUIData(); +} + +OvCore::ECS::Components::UI::CLayoutGroup::~CLayoutGroup() = default; + +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)); + InvalidateLayoutCache(); +} + +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); + InvalidateLayoutCache(); +} + +float OvCore::ECS::Components::UI::CLayoutGroup::GetSpacing() const +{ + return m_spacing; +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::CLayoutGroup::GetComputedSize() const +{ + return GetResolvedLayout().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); + InvalidateLayoutCache(); +} + +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)); + InvalidateLayoutCache(); +} + +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)); + InvalidateLayoutCache(); +} + +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; + InvalidateLayoutCache(); +} + +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; + InvalidateLayoutCache(); +} + +bool OvCore::ECS::Components::UI::CLayoutGroup::GetControlChildrenHeight() const +{ + return m_controlChildrenHeight; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetForceExpandWidth(bool p_forceExpandWidth) +{ + m_forceExpandWidth = p_forceExpandWidth; + InvalidateLayoutCache(); +} + +bool OvCore::ECS::Components::UI::CLayoutGroup::GetForceExpandWidth() const +{ + return m_forceExpandWidth; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::SetForceExpandHeight(bool p_forceExpandHeight) +{ + m_forceExpandHeight = p_forceExpandHeight; + InvalidateLayoutCache(); +} + +bool OvCore::ECS::Components::UI::CLayoutGroup::GetForceExpandHeight() const +{ + return m_forceExpandHeight; +} + +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; + } + + if (const auto childLayout = GetChildLayout(p_child); childLayout && childLayout->valid) + { + return childLayout->offset; + } + + return OvMaths::FVector2::Zero; +} + +std::optional OvCore::ECS::Components::UI::CLayoutGroup::GetChildLayout(const ECS::Actor& p_child) const +{ + if (p_child.GetParent() != &owner) + { + return std::nullopt; + } + + for (const auto& childLayout : GetResolvedLayout().children) + { + if (childLayout.actor == &p_child) + { + return childLayout; + } + } + + return std::nullopt; +} + +std::vector OvCore::ECS::Components::UI::CLayoutGroup::GetChildOffsets() const +{ + const auto& childLayouts = GetResolvedLayout().children; + + std::vector offsets; + offsets.reserve(childLayouts.size()); + + for (const auto& childLayout : childLayouts) + { + offsets.emplace_back(childLayout.actor, childLayout.offset); + } + + return offsets; +} + +std::vector OvCore::ECS::Components::UI::CLayoutGroup::GetChildLayouts() const +{ + return GetResolvedLayout().children; +} + +OvCore::ECS::Components::UI::CLayoutGroup::LayoutCacheInput OvCore::ECS::Components::UI::CLayoutGroup::BuildLayoutCacheInput() const +{ + LayoutCacheInput input; + const auto settings = CreateLayoutSettings(*this, owner); + + input.signature = { + .direction = settings.direction, + .spacing = settings.spacing, + .padding = settings.padding, + .horizontalAlignment = settings.horizontalAlignment, + .verticalAlignment = settings.verticalAlignment, + .controlChildrenWidth = settings.controlChildrenWidth, + .controlChildrenHeight = settings.controlChildrenHeight, + .forceExpandWidth = settings.forceExpandWidth, + .forceExpandHeight = settings.forceExpandHeight, + .containerSize = settings.containerSize, + .pivot = settings.pivot + }; + + const auto& children = owner.GetChildren(); + input.children.reserve(children.size()); + input.signature.children.reserve(children.size()); + + for (const auto child : children) + { + if (!child || !child->IsActive()) + { + continue; + } + + const auto size = GetLayoutSize(*child); + if (!size) + { + continue; + } + + input.children.push_back({ + .actor = child, + .preferredSize = size.value() + }); + input.signature.children.push_back({ + .actor = child, + .preferredSize = size.value() + }); + } + + return input; +} + +const OvCore::ECS::Components::UI::CLayoutGroup::LayoutCache& OvCore::ECS::Components::UI::CLayoutGroup::GetResolvedLayout() const +{ + const auto input = BuildLayoutCacheInput(); + + if (m_layoutCache.valid && HasSameLayoutSignature(m_layoutCache.signature, input.signature)) + { + return m_layoutCache; + } + + std::vector layoutChildren; + layoutChildren.reserve(input.children.size()); + + for (const auto& child : input.children) + { + layoutChildren.push_back({ + .actor = child.actor, + .preferredSize = child.preferredSize + }); + } + + const auto layoutResult = ClayLayoutSolver::Solve(*m_layoutSolverContext, CreateLayoutSettings(*this, owner), layoutChildren); + + m_layoutCache.valid = true; + m_layoutCache.signature = input.signature; + m_layoutCache.size = layoutResult.size; + m_layoutCache.children.clear(); + m_layoutCache.children.reserve(layoutResult.children.size()); + + for (const auto& child : layoutResult.children) + { + m_layoutCache.children.push_back({ + .actor = child.actor, + .offset = child.offset, + .size = child.size, + .hasDirectWidth = GetControlChildrenWidth() || GetForceExpandWidth(), + .hasDirectHeight = GetControlChildrenHeight() || GetForceExpandHeight(), + .valid = child.valid + }); + } + + return m_layoutCache; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::InvalidateLayoutCache() const +{ + m_layoutCache.valid = false; + m_layoutCache.children.clear(); +} + +bool OvCore::ECS::Components::UI::CLayoutGroup::HasSameLayoutSignature( + const OvCore::ECS::Components::UI::CLayoutGroup::LayoutCacheSignature& p_lhs, + const OvCore::ECS::Components::UI::CLayoutGroup::LayoutCacheSignature& p_rhs +) +{ + if ( + p_lhs.direction != p_rhs.direction || + p_lhs.spacing != p_rhs.spacing || + p_lhs.padding.x != p_rhs.padding.x || + p_lhs.padding.y != p_rhs.padding.y || + p_lhs.padding.z != p_rhs.padding.z || + p_lhs.padding.w != p_rhs.padding.w || + p_lhs.horizontalAlignment != p_rhs.horizontalAlignment || + p_lhs.verticalAlignment != p_rhs.verticalAlignment || + p_lhs.controlChildrenWidth != p_rhs.controlChildrenWidth || + p_lhs.controlChildrenHeight != p_rhs.controlChildrenHeight || + p_lhs.forceExpandWidth != p_rhs.forceExpandWidth || + p_lhs.forceExpandHeight != p_rhs.forceExpandHeight || + p_lhs.containerSize.x != p_rhs.containerSize.x || + p_lhs.containerSize.y != p_rhs.containerSize.y || + p_lhs.pivot.x != p_rhs.pivot.x || + p_lhs.pivot.y != p_rhs.pivot.y || + p_lhs.children.size() != p_rhs.children.size() + ) + { + return false; + } + + for (size_t i = 0; i < p_lhs.children.size(); ++i) + { + const auto& leftChild = p_lhs.children[i]; + const auto& rightChild = p_rhs.children[i]; + + if ( + leftChild.actor != rightChild.actor || + leftChild.preferredSize.x != rightChild.preferredSize.x || + leftChild.preferredSize.y != rightChild.preferredSize.y + ) + { + return false; + } + } + + return true; +} + +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::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); + Helpers::Serializer::SerializeBoolean(p_doc, p_node, "force_expand_width", m_forceExpandWidth); + Helpers::Serializer::SerializeBoolean(p_doc, p_node, "force_expand_height", m_forceExpandHeight); +} + +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("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); + } + + if (p_node->FirstChildElement("force_expand_width")) + { + auto forceExpandWidth = m_forceExpandWidth; + Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "force_expand_width", forceExpandWidth); + SetForceExpandWidth(forceExpandWidth); + } + + if (p_node->FirstChildElement("force_expand_height")) + { + auto forceExpandHeight = m_forceExpandHeight; + Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "force_expand_height", forceExpandHeight); + SetForceExpandHeight(forceExpandHeight); + } +} + +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::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); } + ); + + Helpers::GUIDrawer::DrawBoolean( + p_root, + "Force Expand Width", + [this]() { return GetForceExpandWidth(); }, + [this](bool p_value) { SetForceExpandWidth(p_value); } + ); + + Helpers::GUIDrawer::DrawBoolean( + p_root, + "Force Expand Height", + [this]() { return GetForceExpandHeight(); }, + [this](bool p_value) { SetForceExpandHeight(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..2166c5dd7 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CText.cpp @@ -0,0 +1,562 @@ +/** +* @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 +#include +#include + +namespace +{ + constexpr float kMinimumFontSize = 1.0f; + constexpr float kSizeUpdateEpsilon = 0.0001f; + 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; + } + } + + bool IsNearlyEqual(float p_left, float p_right) + { + return std::abs(p_left - p_right) <= kSizeUpdateEpsilon; + } + + bool IsSameSize(const OvMaths::FVector2& p_left, const OvMaths::FVector2& p_right) + { + return IsNearlyEqual(p_left.x, p_right.x) && IsNearlyEqual(p_left.y, p_right.y); + } + + OvCore::ECS::Components::UI::TextLayoutEngine::EHorizontalAlignment ToTextLayoutAlignment( + OvCore::ECS::Components::UI::CText::EHorizontalAlignment p_alignment + ) + { + using EHorizontalAlignment = OvCore::ECS::Components::UI::CText::EHorizontalAlignment; + using ELayoutAlignment = OvCore::ECS::Components::UI::TextLayoutEngine::EHorizontalAlignment; + + switch (p_alignment) + { + case EHorizontalAlignment::CENTER: + return ELayoutAlignment::CENTER; + case EHorizontalAlignment::RIGHT: + return ELayoutAlignment::RIGHT; + case EHorizontalAlignment::LEFT: + default: + return ELayoutAlignment::LEFT; + } + } + + OvCore::ECS::Components::UI::TextLayoutEngine::EVerticalAlignment ToTextLayoutAlignment( + OvCore::ECS::Components::UI::CText::EVerticalAlignment p_alignment + ) + { + using EVerticalAlignment = OvCore::ECS::Components::UI::CText::EVerticalAlignment; + using ELayoutAlignment = OvCore::ECS::Components::UI::TextLayoutEngine::EVerticalAlignment; + + switch (p_alignment) + { + case EVerticalAlignment::CENTER: + return ELayoutAlignment::CENTER; + case EVerticalAlignment::BOTTOM: + return ELayoutAlignment::BOTTOM; + case EVerticalAlignment::TOP: + default: + return ELayoutAlignment::TOP; + } + } +} + +OvCore::ECS::Components::UI::CText::CText(ECS::Actor& p_owner) : +AComponent(p_owner) +{ + m_fontPath = Global::ServiceLocator::Get().GetDefinition().defaultFontPath; + owner.transform.EnableUIData(); +} + +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; + m_unavailableFontPath.clear(); + MarkMeshDirty(); + MarkMaterialSourceDirty(); +} + +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(); + MarkMaterialSourceDirty(); +} + +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); + MarkMaterialColorDirty(); +} + +const OvMaths::FVector4& OvCore::ECS::Components::UI::CText::GetColor() const +{ + return m_color; +} + +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::Resources::Mesh* OvCore::ECS::Components::UI::CText::GetMesh(const OvMaths::FVector2& p_resolvedSize) const +{ + RebuildMesh(p_resolvedSize); + 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 +{ + RebuildLayout(); + return m_size; +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::CText::GetSize(const OvMaths::FVector2& p_resolvedSize) const +{ + RebuildLayout(p_resolvedSize); + 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::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("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::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() || m_fontPath == "?") + { + return nullptr; + } + + auto& fontManager = Global::ServiceLocator::Get(); + if (m_unavailableFontPath == m_fontPath) + { + auto* font = fontManager.GetResource(m_fontPath, false); + if (font) + { + m_unavailableFontPath.clear(); + } + + return font; + } + + auto* font = fontManager.GetResource(m_fontPath); + if (!font) + { + m_unavailableFontPath = m_fontPath; + } + + return font; +} + +void OvCore::ECS::Components::UI::CText::MarkMeshDirty() +{ + m_layoutDirty = true; + m_meshDirty = true; +} + +void OvCore::ECS::Components::UI::CText::MarkMaterialSourceDirty() +{ + m_materialSourceDirty = true; + m_materialColorDirty = true; +} + +void OvCore::ECS::Components::UI::CText::MarkMaterialColorDirty() +{ + m_materialColorDirty = true; +} + +void OvCore::ECS::Components::UI::CText::RebuildLayout() const +{ + RebuildLayout(owner.transform.GetUISize()); +} + +void OvCore::ECS::Components::UI::CText::RebuildLayout(const OvMaths::FVector2& p_uiSize) const +{ + if (!m_layoutDirty && IsSameSize(m_lastLayoutUISize, p_uiSize)) + { + return; + } + + auto* font = GetFont(); + if (!font) + { + m_layout = {}; + m_size = OvMaths::FVector2::Zero; + m_lastLayoutUISize = p_uiSize; + m_layoutDirty = true; + m_meshDirty = true; + return; + } + + m_layoutDirty = false; + m_lastLayoutUISize = p_uiSize; + + m_layout = TextLayoutEngine::Layout({ + .text = m_text, + .font = font, + .fontSize = m_fontSize, + .uiSize = p_uiSize, + .horizontalAlignment = ToTextLayoutAlignment(m_horizontalAlignment), + .verticalAlignment = ToTextLayoutAlignment(m_verticalAlignment) + }); + + m_size = m_layout.size; +} + +void OvCore::ECS::Components::UI::CText::RebuildMesh() const +{ + RebuildMesh(owner.transform.GetUISize()); +} + +void OvCore::ECS::Components::UI::CText::RebuildMesh(const OvMaths::FVector2& p_uiSize) const +{ + if (!m_meshDirty && IsSameSize(m_lastMeshUISize, p_uiSize)) + { + return; + } + + RebuildLayout(p_uiSize); + if (m_layoutDirty) + { + m_mesh.reset(); + return; + } + + m_meshDirty = false; + m_lastMeshUISize = p_uiSize; + + auto textMesh = TextMeshBuilder::Build(m_layout); + + m_size = textMesh.size; + m_lastLayoutUISize = p_uiSize; + m_mesh = std::move(textMesh.mesh); +} + +void OvCore::ECS::Components::UI::CText::RefreshMaterial() +{ + if (!m_material) + { + m_material = std::make_unique(); + MarkMaterialSourceDirty(); + } + + if (!m_materialSourceDirty) + { + const auto& textMaterialPath = Global::ServiceLocator::Get().GetDefinition().textMaterialPath; + auto* defaultMaterial = textMaterialPath.empty() ? + nullptr : + Global::ServiceLocator::Get().GetResource(textMaterialPath, false); + auto* currentShader = defaultMaterial && defaultMaterial->HasShader() ? defaultMaterial->GetShader() : nullptr; + auto* currentFont = m_fontPath.empty() || m_fontPath == "?" ? + nullptr : + Global::ServiceLocator::Get().GetResource(m_fontPath, false); + + if ( + currentShader != m_materialShader || + currentFont != m_materialFont || + (currentFont && currentFont->GetRevision() != m_materialFontRevision) + ) + { + MarkMaterialSourceDirty(); + } + } + + if (m_materialSourceDirty) + { + const auto& textMaterialPath = Global::ServiceLocator::Get().GetDefinition().textMaterialPath; + auto* defaultMaterial = textMaterialPath.empty() ? + nullptr : + Global::ServiceLocator::Get().GetResource(textMaterialPath); + auto* defaultShader = defaultMaterial && defaultMaterial->HasShader() ? defaultMaterial->GetShader() : nullptr; + auto* font = GetFont(); + + m_materialFont = font; + m_materialShader = defaultShader; + m_materialFontRevision = font ? font->GetRevision() : 0; + + if (!defaultShader || !font || !font->EnsureEmbeddedMaterial(defaultShader, m_fontSize)) + { + if (m_material->HasShader()) + { + m_material->SetShader(nullptr); + } + + m_materialColorDirty = true; + return; + } + + auto* embeddedMaterial = font->GetEmbeddedMaterial(m_fontSize); + if (!embeddedMaterial || !embeddedMaterial->IsValid()) + { + if (m_material->HasShader()) + { + m_material->SetShader(nullptr); + } + + m_materialColorDirty = true; + return; + } + + *m_material = *embeddedMaterial; + m_materialFontRevision = font->GetRevision(); + m_materialSourceDirty = false; + m_materialColorDirty = true; + } + + if (!m_material->IsValid()) + { + return; + } + + if (m_materialColorDirty) + { + m_material->TrySetProperty(kColorUniform, m_color); + m_materialColorDirty = false; + } +} 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/ECS/Components/UI/ClayLayoutSolver.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/ClayLayoutSolver.cpp new file mode 100644 index 000000000..51ed43500 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/ClayLayoutSolver.cpp @@ -0,0 +1,762 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#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 + +namespace +{ + constexpr float kMinimumLayoutSize = 1.0f; + constexpr float kMaximumLayoutSize = 1'000'000.0f; + constexpr float kMinimumSpacing = 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 int32_t kMinimumElementCapacity = 64; + constexpr int32_t kSolverElementOverhead = 8; + + std::mutex& GetClayApiMutex() + { + // Clay stores its current context globally, so isolate that mutable API behind one lock. + static std::mutex mutex; + return mutex; + } + + struct ClayLayoutPassResult + { + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + std::vector childBoxes; + std::vector childFound; + }; + + class ScopedClayContext + { + public: + explicit ScopedClayContext(Clay_Context* p_context) : + m_previousContext(Clay_GetCurrentContext()) + { + Clay_SetCurrentContext(p_context); + } + + ~ScopedClayContext() + { + Clay_SetCurrentContext(m_previousContext); + } + + private: + Clay_Context* m_previousContext = nullptr; + }; + + class ClaySolverRuntime + { + public: + Clay_Context* GetContext(size_t p_childCount) + { + const auto requiredCapacity = std::max( + kMinimumElementCapacity, + static_cast(p_childCount) + kSolverElementOverhead + ); + + if (!m_context || requiredCapacity > m_elementCapacity) + { + Initialize(requiredCapacity); + } + + return m_context; + } + + private: + static void HandleError(Clay_ErrorData p_errorData) + { + auto* runtime = static_cast(p_errorData.userData); + if (!runtime) + { + return; + } + + runtime->m_lastError.assign(p_errorData.errorText.chars, p_errorData.errorText.length); + } + + void Initialize(int32_t p_elementCapacity) + { + std::lock_guard lock(GetClayApiMutex()); + auto* previousContext = Clay_GetCurrentContext(); + + Clay_SetCurrentContext(nullptr); + Clay_SetMaxElementCount(p_elementCapacity); + + const auto memorySize = Clay_MinMemorySize(); + m_memory.resize(memorySize); + auto arena = Clay_CreateArenaWithCapacityAndMemory(m_memory.size(), m_memory.data()); + m_context = Clay_Initialize( + arena, + { kMaximumLayoutSize, kMaximumLayoutSize }, + { &ClaySolverRuntime::HandleError, this } + ); + + m_elementCapacity = p_elementCapacity; + + if (m_context) + { + ScopedClayContext useContext(m_context); + Clay_SetCullingEnabled(false); + } + + Clay_SetCurrentContext(previousContext); + } + + private: + Clay_Context* m_context = nullptr; + int32_t m_elementCapacity = 0; + std::vector m_memory; + std::string m_lastError; + }; + + float KeepFinite(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? p_value : p_fallback; + } + + float ClampNonNegative(float p_value) + { + return std::max(KeepFinite(p_value, 0.0f), 0.0f); + } + + uint16_t ToClaySpacing(float p_spacing) + { + return static_cast(std::clamp(KeepFinite(p_spacing, kMinimumSpacing), kMinimumSpacing, kMaximumSpacing)); + } + + Clay_Padding ToClayPadding(const OvMaths::FVector4& p_padding) + { + return { + .left = static_cast(std::clamp(KeepFinite(p_padding.x, kMinimumPadding), kMinimumPadding, kMaximumPadding)), + .right = static_cast(std::clamp(KeepFinite(p_padding.y, kMinimumPadding), kMinimumPadding, kMaximumPadding)), + .top = static_cast(std::clamp(KeepFinite(p_padding.z, kMinimumPadding), kMinimumPadding, kMaximumPadding)), + .bottom = static_cast(std::clamp(KeepFinite(p_padding.w, kMinimumPadding), kMinimumPadding, kMaximumPadding)) + }; + } + + Clay_LayoutDirection ToClayDirection(OvCore::ECS::Components::UI::CLayoutGroup::EDirection p_direction) + { + return p_direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::VERTICAL ? + CLAY_TOP_TO_BOTTOM : + CLAY_LEFT_TO_RIGHT; + } + + Clay_LayoutAlignmentX ToClayHorizontalAlignment(OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment p_alignment) + { + using EHorizontalAlignment = OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment; + + switch (p_alignment) + { + case EHorizontalAlignment::LEFT: + return CLAY_ALIGN_X_LEFT; + case EHorizontalAlignment::RIGHT: + return CLAY_ALIGN_X_RIGHT; + case EHorizontalAlignment::CENTER: + default: + return CLAY_ALIGN_X_CENTER; + } + } + + Clay_LayoutAlignmentY ToClayVerticalAlignment(OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment p_alignment) + { + using EVerticalAlignment = OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment; + + switch (p_alignment) + { + case EVerticalAlignment::TOP: + return CLAY_ALIGN_Y_TOP; + case EVerticalAlignment::BOTTOM: + return CLAY_ALIGN_Y_BOTTOM; + case EVerticalAlignment::CENTER: + default: + return CLAY_ALIGN_Y_CENTER; + } + } + + Clay_SizingAxis MakeFixedSizing(float p_size) + { + return CLAY_SIZING_FIXED(ClampNonNegative(p_size)); + } + + Clay_SizingAxis MakeFitSizing(float p_minSize) + { + return CLAY_SIZING_FIT(std::max(ClampNonNegative(p_minSize), kMinimumLayoutSize), kMaximumLayoutSize); + } + + Clay_SizingAxis MakeGrowSizing(float p_minSize) + { + return CLAY_SIZING_GROW(ClampNonNegative(p_minSize), kMaximumLayoutSize); + } + + Clay_SizingAxis MakeChildSizing(float p_preferredSize, bool p_controlSize, bool p_forceExpand) + { + if (p_controlSize) + { + return MakeGrowSizing(0.0f); + } + + if (p_forceExpand) + { + return MakeGrowSizing(p_preferredSize); + } + + return MakeFixedSizing(p_preferredSize); + } + + Clay_ElementDeclaration CreateContainerDeclaration( + const OvCore::ECS::Components::UI::ClayLayoutSettings& p_settings, + Clay_SizingAxis p_widthSizing, + Clay_SizingAxis p_heightSizing + ) + { + Clay_ElementDeclaration declaration{}; + declaration.layout.sizing.width = p_widthSizing; + declaration.layout.sizing.height = p_heightSizing; + declaration.layout.padding = ToClayPadding(p_settings.padding); + declaration.layout.childGap = ToClaySpacing(p_settings.spacing); + declaration.layout.childAlignment.x = ToClayHorizontalAlignment(p_settings.horizontalAlignment); + declaration.layout.childAlignment.y = ToClayVerticalAlignment(p_settings.verticalAlignment); + declaration.layout.layoutDirection = ToClayDirection(p_settings.direction); + return declaration; + } + + Clay_ElementDeclaration CreateChildDeclaration( + const OvCore::ECS::Components::UI::ClayLayoutSettings& p_settings, + const OvCore::ECS::Components::UI::ClayLayoutChildInput& p_child, + bool p_useControlledSizing + ) + { + Clay_ElementDeclaration declaration{}; + declaration.layout.sizing.width = p_useControlledSizing ? + MakeChildSizing(p_child.preferredSize.x, p_settings.controlChildrenWidth, p_settings.forceExpandWidth) : + MakeFixedSizing(p_child.preferredSize.x); + declaration.layout.sizing.height = p_useControlledSizing ? + MakeChildSizing(p_child.preferredSize.y, p_settings.controlChildrenHeight, p_settings.forceExpandHeight) : + MakeFixedSizing(p_child.preferredSize.y); + return declaration; + } + + ClayLayoutPassResult RunClayPass( + Clay_Context* p_context, + const OvCore::ECS::Components::UI::ClayLayoutSettings& p_settings, + const std::vector& p_children, + Clay_SizingAxis p_containerWidthSizing, + Clay_SizingAxis p_containerHeightSizing, + const OvMaths::FVector2& p_rootSize, + bool p_useControlledChildSizing + ) + { + std::lock_guard lock(GetClayApiMutex()); + ScopedClayContext useContext(p_context); + + Clay_SetLayoutDimensions({ + std::max(ClampNonNegative(p_rootSize.x), kMinimumLayoutSize), + std::max(ClampNonNegative(p_rootSize.y), kMinimumLayoutSize) + }); + + const auto containerId = CLAY_ID("Overload_LayoutGroup_Container"); + const auto childIdBase = CLAY_STRING("Overload_LayoutGroup_Child"); + std::vector childIds; + childIds.reserve(p_children.size()); + + Clay_BeginLayout(); + + CLAY(containerId, CreateContainerDeclaration(p_settings, p_containerWidthSizing, p_containerHeightSizing)) + { + for (size_t i = 0; i < p_children.size(); ++i) + { + const auto childId = Clay_GetElementIdWithIndex(childIdBase, static_cast(i)); + childIds.push_back(childId); + CLAY(childId, CreateChildDeclaration(p_settings, p_children[i], p_useControlledChildSizing)) + { + } + } + } + + Clay_EndLayout(0.0f); + + ClayLayoutPassResult result; + + const auto containerData = Clay_GetElementData(containerId); + if (!containerData.found) + { + return result; + } + + result.size = { + std::max(containerData.boundingBox.width, kMinimumLayoutSize), + std::max(containerData.boundingBox.height, kMinimumLayoutSize) + }; + + result.childBoxes.reserve(childIds.size()); + result.childFound.reserve(childIds.size()); + for (const auto& childId : childIds) + { + const auto childData = Clay_GetElementData(childId); + result.childBoxes.push_back(childData.found ? childData.boundingBox : Clay_BoundingBox{}); + result.childFound.push_back(childData.found); + } + + return result; + } + + OvMaths::FVector2 GetRootSize(const OvMaths::FVector2& p_containerSize, const OvMaths::FVector2& p_preferredSize) + { + return { + p_containerSize.x > 0.0f ? p_containerSize.x : std::max(p_preferredSize.x, kMinimumLayoutSize), + p_containerSize.y > 0.0f ? p_containerSize.y : std::max(p_preferredSize.y, kMinimumLayoutSize) + }; + } + + OvMaths::FVector2 ToChildOffset( + const OvMaths::FVector2& p_childTopLeft, + const OvMaths::FVector2& p_childSize, + const OvMaths::FVector2& p_layoutSize, + const OvMaths::FVector2& p_pivot + ) + { + const float centerX = p_childTopLeft.x + p_childSize.x * 0.5f; + const float centerY = p_childTopLeft.y + p_childSize.y * 0.5f; + const auto halfSize = p_layoutSize * 0.5f; + const OvMaths::FVector2 pivotOffset = { + -p_pivot.x * halfSize.x, + p_pivot.y * halfSize.y + }; + + return { + centerX - halfSize.x + pivotOffset.x, + halfSize.y - centerY + pivotOffset.y + }; + } + + float GetHorizontalAlignmentOffset( + OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment p_alignment, + float p_availableWidth, + float p_contentWidth + ) + { + const float extraSpace = std::max(0.0f, p_availableWidth - p_contentWidth); + + switch (p_alignment) + { + case OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment::LEFT: + return 0.0f; + case OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment::RIGHT: + return extraSpace; + case OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment::CENTER: + default: + return extraSpace * 0.5f; + } + } + + float GetVerticalAlignmentOffset( + OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment p_alignment, + float p_availableHeight, + float p_contentHeight + ) + { + const float extraSpace = std::max(0.0f, p_availableHeight - p_contentHeight); + + switch (p_alignment) + { + case OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment::TOP: + return 0.0f; + case OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment::BOTTOM: + return extraSpace; + case OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment::CENTER: + default: + return extraSpace * 0.5f; + } + } + + OvMaths::FVector2 GetChildrenContentSize( + const std::vector& p_children, + OvCore::ECS::Components::UI::CLayoutGroup::EDirection p_direction, + float p_spacing + ) + { + if (p_children.empty()) + { + return OvMaths::FVector2::Zero; + } + + OvMaths::FVector2 result = OvMaths::FVector2::Zero; + + for (const auto& child : p_children) + { + if (!child.valid) + { + continue; + } + + if (p_direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL) + { + result.x += child.size.x; + result.y = std::max(result.y, child.size.y); + } + else + { + result.x = std::max(result.x, child.size.x); + result.y += child.size.y; + } + } + + const auto validChildCount = static_cast(std::count_if(p_children.begin(), p_children.end(), [](const auto& p_child) + { + return p_child.valid; + })); + const float gapSize = p_spacing * std::max(validChildCount - 1.0f, 0.0f); + + if (p_direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL) + { + result.x += gapSize; + } + else + { + result.y += gapSize; + } + + return result; + } + + float GetValidChildCount( + const std::vector& p_children + ) + { + return static_cast(std::count_if(p_children.begin(), p_children.end(), [](const auto& p_child) + { + return p_child.valid; + })); + } + + void ApplyControlledSizing( + OvCore::ECS::Components::UI::ClayLayoutResult& p_result, + const OvCore::ECS::Components::UI::ClayLayoutSettings& p_settings + ) + { + if ( + !p_settings.controlChildrenWidth && + !p_settings.controlChildrenHeight && + !p_settings.forceExpandWidth && + !p_settings.forceExpandHeight + ) + { + return; + } + + const auto childCount = GetValidChildCount(p_result.children); + if (childCount <= 0.0f) + { + return; + } + + const auto padding = ToClayPadding(p_settings.padding); + const float spacing = static_cast(ToClaySpacing(p_settings.spacing)); + const float availableWidth = std::max(0.0f, p_result.size.x - padding.left - padding.right); + const float availableHeight = std::max(0.0f, p_result.size.y - padding.top - padding.bottom); + + float controlledWidth = 0.0f; + if (p_settings.controlChildrenWidth || p_settings.forceExpandWidth) + { + if (p_settings.direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL) + { + float occupiedWidth = 0.0f; + for (const auto& child : p_result.children) + { + if (child.valid) + { + occupiedWidth += child.size.x; + } + } + + const float availableChildWidth = std::max(availableWidth - std::max(childCount - 1.0f, 0.0f) * spacing, 0.0f); + controlledWidth = p_settings.controlChildrenWidth ? + availableChildWidth / childCount : + std::max((availableChildWidth - occupiedWidth) / childCount, 0.0f); + } + else + { + controlledWidth = availableWidth; + } + } + + float controlledHeight = 0.0f; + if (p_settings.controlChildrenHeight || p_settings.forceExpandHeight) + { + if (p_settings.direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::VERTICAL) + { + float occupiedHeight = 0.0f; + for (const auto& child : p_result.children) + { + if (child.valid) + { + occupiedHeight += child.size.y; + } + } + + const float availableChildHeight = std::max(availableHeight - std::max(childCount - 1.0f, 0.0f) * spacing, 0.0f); + controlledHeight = p_settings.controlChildrenHeight ? + availableChildHeight / childCount : + std::max((availableChildHeight - occupiedHeight) / childCount, 0.0f); + } + else + { + controlledHeight = availableHeight; + } + } + + for (auto& child : p_result.children) + { + if (!child.valid) + { + continue; + } + + if (p_settings.controlChildrenWidth) + { + child.size.x = controlledWidth; + } + else if (p_settings.forceExpandWidth) + { + child.size.x = p_settings.direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL ? + child.size.x + controlledWidth : + std::max(child.size.x, controlledWidth); + } + + if (p_settings.controlChildrenHeight) + { + child.size.y = controlledHeight; + } + else if (p_settings.forceExpandHeight) + { + child.size.y = p_settings.direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::VERTICAL ? + child.size.y + controlledHeight : + std::max(child.size.y, controlledHeight); + } + } + } + + void ApplyAlignedOffsets( + OvCore::ECS::Components::UI::ClayLayoutResult& p_result, + const OvCore::ECS::Components::UI::ClayLayoutSettings& p_settings + ) + { + const auto padding = ToClayPadding(p_settings.padding); + const float spacing = static_cast(ToClaySpacing(p_settings.spacing)); + const float availableWidth = std::max(0.0f, p_result.size.x - padding.left - padding.right); + const float availableHeight = std::max(0.0f, p_result.size.y - padding.top - padding.bottom); + const auto contentSize = GetChildrenContentSize(p_result.children, p_settings.direction, spacing); + + if (p_settings.direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL) + { + float childLeft = static_cast(padding.left) + + GetHorizontalAlignmentOffset(p_settings.horizontalAlignment, availableWidth, contentSize.x); + + for (auto& child : p_result.children) + { + if (!child.valid) + { + continue; + } + + const float childTop = static_cast(padding.top) + + GetVerticalAlignmentOffset(p_settings.verticalAlignment, availableHeight, child.size.y); + child.offset = ToChildOffset({ childLeft, childTop }, child.size, p_result.size, p_settings.pivot); + childLeft += child.size.x + spacing; + } + } + else + { + float childTop = static_cast(padding.top) + + GetVerticalAlignmentOffset(p_settings.verticalAlignment, availableHeight, contentSize.y); + + for (auto& child : p_result.children) + { + if (!child.valid) + { + continue; + } + + const float childLeft = static_cast(padding.left) + + GetHorizontalAlignmentOffset(p_settings.horizontalAlignment, availableWidth, child.size.x); + child.offset = ToChildOffset({ childLeft, childTop }, child.size, p_result.size, p_settings.pivot); + childTop += child.size.y + spacing; + } + } + } +} + +struct OvCore::ECS::Components::UI::ClayLayoutSolverContext::Impl : ClaySolverRuntime +{ +}; + +OvCore::ECS::Components::UI::ClayLayoutSolverContext::ClayLayoutSolverContext() : +m_impl(std::make_unique()) +{ +} + +OvCore::ECS::Components::UI::ClayLayoutSolverContext::~ClayLayoutSolverContext() = default; + +OvCore::ECS::Components::UI::ClayLayoutSolverContext::ClayLayoutSolverContext(ClayLayoutSolverContext&&) noexcept = default; + +OvCore::ECS::Components::UI::ClayLayoutSolverContext& OvCore::ECS::Components::UI::ClayLayoutSolverContext::operator=(ClayLayoutSolverContext&&) noexcept = default; + +OvCore::ECS::Components::UI::ClayLayoutMeasurement OvCore::ECS::Components::UI::ClayLayoutSolver::Measure( + ClayLayoutSolverContext& p_context, + const ClayLayoutSettings& p_settings, + const std::vector& p_children +) +{ + ClayLayoutMeasurement measurement; + measurement.settings = p_settings; + measurement.settings.containerSize.x = ClampNonNegative(measurement.settings.containerSize.x); + measurement.settings.containerSize.y = ClampNonNegative(measurement.settings.containerSize.y); + + auto* context = p_context.m_impl ? p_context.m_impl->GetContext(p_children.size()) : nullptr; + if (!context) + { + return measurement; + } + + const Clay_SizingAxis measuredWidthSizing = measurement.settings.containerSize.x > 0.0f ? + MakeFixedSizing(measurement.settings.containerSize.x) : + MakeFitSizing(kMinimumLayoutSize); + const Clay_SizingAxis measuredHeightSizing = measurement.settings.containerSize.y > 0.0f ? + MakeFixedSizing(measurement.settings.containerSize.y) : + MakeFitSizing(kMinimumLayoutSize); + + const auto preferredPass = RunClayPass( + context, + measurement.settings, + p_children, + measuredWidthSizing, + measuredHeightSizing, + { kMaximumLayoutSize, kMaximumLayoutSize }, + false + ); + + measurement.preferredSize = preferredPass.size; + measurement.valid = true; + return measurement; +} + +OvCore::ECS::Components::UI::ClayLayoutSolution OvCore::ECS::Components::UI::ClayLayoutSolver::SolveLayout( + ClayLayoutSolverContext& p_context, + const ClayLayoutMeasurement& p_measurement, + const std::vector& p_children +) +{ + ClayLayoutSolution solution; + solution.settings = p_measurement.settings; + solution.preferredSize = p_measurement.preferredSize; + + if (!p_measurement.valid) + { + return solution; + } + + auto* context = p_context.m_impl ? p_context.m_impl->GetContext(p_children.size()) : nullptr; + if (!context) + { + return solution; + } + + const auto& settings = p_measurement.settings; + + const Clay_SizingAxis finalWidthSizing = settings.containerSize.x > 0.0f ? + MakeFixedSizing(settings.containerSize.x) : + MakeFitSizing(p_measurement.preferredSize.x); + const Clay_SizingAxis finalHeightSizing = settings.containerSize.y > 0.0f ? + MakeFixedSizing(settings.containerSize.y) : + MakeFitSizing(p_measurement.preferredSize.y); + + const auto finalPass = RunClayPass( + context, + settings, + p_children, + finalWidthSizing, + finalHeightSizing, + GetRootSize(settings.containerSize, p_measurement.preferredSize), + true + ); + + solution.result.size = GetRootSize(settings.containerSize, p_measurement.preferredSize); + solution.result.children.reserve(p_children.size()); + + for (size_t i = 0; i < p_children.size(); ++i) + { + const auto childBox = i < finalPass.childBoxes.size() ? finalPass.childBoxes[i] : Clay_BoundingBox{}; + const bool childValid = + i < finalPass.childFound.size() && + finalPass.childFound[i] && + childBox.width > 0.0f && + childBox.height > 0.0f; + const auto childSize = childValid ? + OvMaths::FVector2{ childBox.width, childBox.height } : + p_children[i].preferredSize; + const bool hasUsableChild = p_children[i].actor && childSize.x > 0.0f && childSize.y > 0.0f; + + solution.result.children.push_back({ + .actor = p_children[i].actor, + .offset = OvMaths::FVector2::Zero, + .size = childSize, + .valid = hasUsableChild + }); + } + + solution.valid = true; + return solution; +} + +OvCore::ECS::Components::UI::ClayLayoutResult OvCore::ECS::Components::UI::ClayLayoutSolver::Postprocess( + const ClayLayoutSolution& p_solution +) +{ + if (!p_solution.valid) + { + return {}; + } + + auto result = p_solution.result; + ApplyControlledSizing(result, p_solution.settings); + ApplyAlignedOffsets(result, p_solution.settings); + return result; +} + +OvCore::ECS::Components::UI::ClayLayoutResult OvCore::ECS::Components::UI::ClayLayoutSolver::Solve( + ClayLayoutSolverContext& p_context, + const ClayLayoutSettings& p_settings, + const std::vector& p_children +) +{ + const auto measurement = Measure(p_context, p_settings, p_children); + const auto solution = SolveLayout(p_context, measurement, p_children); + return Postprocess(solution); +} diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/TextLayoutEngine.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/TextLayoutEngine.cpp new file mode 100644 index 000000000..6ad02fb7c --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/TextLayoutEngine.cpp @@ -0,0 +1,420 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + OvMaths::FVector2 ResolveTextSize(const OvMaths::FVector2& p_contentSize, const OvMaths::FVector2& p_uiSize) + { + return { + p_uiSize.x > 0.0f ? p_uiSize.x : p_contentSize.x, + p_uiSize.y > 0.0f ? p_uiSize.y : p_contentSize.y + }; + } + + bool IsSoftWrapWhitespace(char p_character) + { + return p_character == ' ' || p_character == '\t'; + } + + float GetGlyphAdvance( + const OvRendering::Resources::Font& p_font, + const OvRendering::Resources::Font::Glyph* p_fallbackGlyph, + float p_fontSize, + float p_scale, + char p_character + ) + { + const auto* glyph = p_font.GetGlyph(p_character, p_fontSize); + if (!glyph) + { + glyph = p_fallbackGlyph; + } + + return glyph ? glyph->xAdvance * p_scale : 0.0f; + } + + float MeasureAdvance( + const OvRendering::Resources::Font& p_font, + const OvRendering::Resources::Font::Glyph* p_fallbackGlyph, + float p_fontSize, + float p_scale, + std::string_view p_text, + size_t p_begin, + size_t p_end + ) + { + float width = 0.0f; + for (size_t index = p_begin; index < p_end; ++index) + { + width += GetGlyphAdvance(p_font, p_fallbackGlyph, p_fontSize, p_scale, p_text[index]); + } + + return width; + } + + void AppendWrappedRun( + std::string& p_output, + const OvRendering::Resources::Font& p_font, + const OvRendering::Resources::Font::Glyph* p_fallbackGlyph, + float p_fontSize, + float p_scale, + float p_maxWidth, + std::string_view p_text, + size_t p_begin, + size_t p_end, + float p_width, + float& p_lineWidth, + bool& p_lineHasContent + ) + { + if (p_width <= p_maxWidth || !p_lineHasContent) + { + p_output.append(p_text.data() + p_begin, p_end - p_begin); + p_lineWidth += p_width; + p_lineHasContent = p_end > p_begin; + return; + } + + for (size_t index = p_begin; index < p_end; ++index) + { + const float characterWidth = GetGlyphAdvance(p_font, p_fallbackGlyph, p_fontSize, p_scale, p_text[index]); + if (p_lineHasContent && p_lineWidth + characterWidth > p_maxWidth) + { + p_output += '\n'; + p_lineWidth = 0.0f; + p_lineHasContent = false; + } + + p_output += p_text[index]; + p_lineWidth += characterWidth; + p_lineHasContent = true; + } + } + + std::string WrapTextToWidth( + std::string_view p_text, + const OvRendering::Resources::Font& p_font, + const OvRendering::Resources::Font::Glyph* p_fallbackGlyph, + float p_fontSize, + float p_scale, + float p_maxWidth + ) + { + if (p_maxWidth <= 0.0f) + { + return std::string{ p_text }; + } + + std::string output; + output.reserve(p_text.size()); + + float lineWidth = 0.0f; + float pendingWhitespaceWidth = 0.0f; + std::string pendingWhitespace; + bool lineHasContent = false; + + for (size_t index = 0; index < p_text.size();) + { + const char character = p_text[index]; + if (character == '\r') + { + ++index; + continue; + } + + if (character == '\n') + { + output += '\n'; + lineWidth = 0.0f; + pendingWhitespaceWidth = 0.0f; + pendingWhitespace.clear(); + lineHasContent = false; + ++index; + continue; + } + + if (IsSoftWrapWhitespace(character)) + { + const size_t whitespaceBegin = index; + while (index < p_text.size() && IsSoftWrapWhitespace(p_text[index])) + { + ++index; + } + + if (lineHasContent) + { + pendingWhitespace.append(p_text.data() + whitespaceBegin, index - whitespaceBegin); + pendingWhitespaceWidth += MeasureAdvance( + p_font, + p_fallbackGlyph, + p_fontSize, + p_scale, + p_text, + whitespaceBegin, + index + ); + } + continue; + } + + const size_t wordBegin = index; + while ( + index < p_text.size() && + p_text[index] != '\r' && + p_text[index] != '\n' && + !IsSoftWrapWhitespace(p_text[index]) + ) + { + ++index; + } + + const float wordWidth = MeasureAdvance( + p_font, + p_fallbackGlyph, + p_fontSize, + p_scale, + p_text, + wordBegin, + index + ); + + if (lineHasContent && lineWidth + pendingWhitespaceWidth + wordWidth > p_maxWidth) + { + output += '\n'; + lineWidth = 0.0f; + lineHasContent = false; + } + else if (lineHasContent && !pendingWhitespace.empty()) + { + output += pendingWhitespace; + lineWidth += pendingWhitespaceWidth; + } + + pendingWhitespace.clear(); + pendingWhitespaceWidth = 0.0f; + + AppendWrappedRun( + output, + p_font, + p_fallbackGlyph, + p_fontSize, + p_scale, + p_maxWidth, + p_text, + wordBegin, + index, + wordWidth, + lineWidth, + lineHasContent + ); + } + + return output; + } + + float GetAlignedCenterX( + float p_textWidth, + float p_contentWidth, + OvCore::ECS::Components::UI::TextLayoutEngine::EHorizontalAlignment p_alignment + ) + { + using EHorizontalAlignment = OvCore::ECS::Components::UI::TextLayoutEngine::EHorizontalAlignment; + + switch (p_alignment) + { + case EHorizontalAlignment::CENTER: + return 0.0f; + case EHorizontalAlignment::RIGHT: + return p_textWidth * 0.5f - p_contentWidth * 0.5f; + case EHorizontalAlignment::LEFT: + default: + return -p_textWidth * 0.5f + p_contentWidth * 0.5f; + } + } + + float GetAlignedCenterY( + float p_textHeight, + float p_contentHeight, + OvCore::ECS::Components::UI::TextLayoutEngine::EVerticalAlignment p_alignment + ) + { + using EVerticalAlignment = OvCore::ECS::Components::UI::TextLayoutEngine::EVerticalAlignment; + + switch (p_alignment) + { + case EVerticalAlignment::CENTER: + return 0.0f; + case EVerticalAlignment::BOTTOM: + return -p_textHeight * 0.5f + p_contentHeight * 0.5f; + case EVerticalAlignment::TOP: + default: + return p_textHeight * 0.5f - p_contentHeight * 0.5f; + } + } +} + +OvCore::ECS::Components::UI::TextLayoutEngine::Output OvCore::ECS::Components::UI::TextLayoutEngine::Layout(const Input& p_input) +{ + Output output; + output.size = ResolveTextSize(OvMaths::FVector2::Zero, p_input.uiSize); + + if (!p_input.font || p_input.text.empty() || !p_input.font->EnsurePixelSize(p_input.fontSize)) + { + return output; + } + + const float bakedPixelSize = p_input.font->GetPixelSize(p_input.fontSize); + if (bakedPixelSize <= 0.0f) + { + return output; + } + + const float scale = p_input.fontSize / bakedPixelSize; + const float lineHeight = p_input.font->GetLineHeight(p_input.fontSize); + + struct LineInfo + { + size_t firstGlyph = 0; + size_t lastGlyph = 0; + float minX = std::numeric_limits::max(); + float maxX = std::numeric_limits::lowest(); + bool hasGeometry = false; + }; + + std::vector lines; + lines.push_back({}); + lines.back().firstGlyph = 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 = p_input.font->GetGlyph('?', p_input.fontSize); + const auto wrappedText = WrapTextToWidth( + p_input.text, + *p_input.font, + fallbackGlyph, + p_input.fontSize, + scale, + p_input.uiSize.x + ); + + output.glyphs.reserve(wrappedText.size()); + + for (const char character : wrappedText) + { + if (character == '\r') + { + continue; + } + + if (character == '\n') + { + lines.back().lastGlyph = output.glyphs.size(); + lines.push_back({}); + lines.back().firstGlyph = output.glyphs.size(); + cursorX = 0.0f; + baselineY -= lineHeight * scale; + continue; + } + + const auto* glyph = p_input.font->GetGlyph(character, p_input.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; + + output.glyphs.push_back({ + .left = x0, + .right = x1, + .bottom = bottomY, + .top = topY, + .uMin = glyph->uMin, + .uMax = glyph->uMax, + .vMin = glyph->vMin, + .vMax = glyph->vMax + }); + + 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.lastGlyph = output.glyphs.size(); + + cursorX += glyph->xAdvance * scale; + } + + lines.back().lastGlyph = output.glyphs.size(); + + if (output.glyphs.empty()) + { + return output; + } + + output.contentSize = { + std::max(maxX - minX, 0.0f), + std::max(maxY - minY, 0.0f) + }; + output.size = ResolveTextSize(output.contentSize, p_input.uiSize); + + for (const auto& line : lines) + { + if (!line.hasGeometry || line.lastGlyph <= line.firstGlyph) + { + continue; + } + + const float lineWidth = std::max(line.maxX - line.minX, 0.0f); + const float lineCenterX = line.minX + lineWidth * 0.5f; + const float alignedLineCenterX = GetAlignedCenterX(output.size.x, lineWidth, p_input.horizontalAlignment); + const float lineOffsetX = alignedLineCenterX - lineCenterX; + + for (size_t glyphIndex = line.firstGlyph; glyphIndex < line.lastGlyph; ++glyphIndex) + { + output.glyphs[glyphIndex].left += lineOffsetX; + output.glyphs[glyphIndex].right += lineOffsetX; + } + } + + const float contentCenterY = minY + output.contentSize.y * 0.5f; + const float alignedCenterY = GetAlignedCenterY(output.size.y, output.contentSize.y, p_input.verticalAlignment); + const float globalOffsetY = alignedCenterY - contentCenterY; + + for (auto& glyph : output.glyphs) + { + glyph.bottom += globalOffsetY; + glyph.top += globalOffsetY; + } + + return output; +} diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/TextMeshBuilder.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/TextMeshBuilder.cpp new file mode 100644 index 000000000..79f47add6 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/TextMeshBuilder.cpp @@ -0,0 +1,179 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + float Interpolate(float p_start, float p_end, float p_ratio) + { + return p_start + (p_end - p_start) * p_ratio; + } + + void ClipTextGeometryToSize( + std::vector& p_vertices, + std::vector& p_indices, + const OvMaths::FVector2& p_size + ) + { + if (p_size.x <= 0.0f || p_size.y <= 0.0f) + { + p_vertices.clear(); + p_indices.clear(); + return; + } + + const float rectLeft = -p_size.x * 0.5f; + const float rectRight = p_size.x * 0.5f; + const float rectBottom = -p_size.y * 0.5f; + const float rectTop = p_size.y * 0.5f; + + std::vector clippedVertices; + std::vector clippedIndices; + clippedVertices.reserve(p_vertices.size()); + clippedIndices.reserve(p_indices.size()); + + for (size_t quadStart = 0; quadStart + 3 < p_vertices.size(); quadStart += 4) + { + const auto& bottomLeft = p_vertices[quadStart + 0]; + const auto& bottomRight = p_vertices[quadStart + 1]; + const auto& topRight = p_vertices[quadStart + 2]; + + const float left = bottomLeft.position[0]; + const float right = bottomRight.position[0]; + const float bottom = bottomLeft.position[1]; + const float top = topRight.position[1]; + + if ( + left >= rectRight || + right <= rectLeft || + bottom >= rectTop || + top <= rectBottom || + right <= left || + top <= bottom + ) + { + continue; + } + + const float clippedLeft = std::max(left, rectLeft); + const float clippedRight = std::min(right, rectRight); + const float clippedBottom = std::max(bottom, rectBottom); + const float clippedTop = std::min(top, rectTop); + + const float horizontalRatioLeft = (clippedLeft - left) / (right - left); + const float horizontalRatioRight = (clippedRight - left) / (right - left); + const float verticalRatioBottom = (clippedBottom - bottom) / (top - bottom); + const float verticalRatioTop = (clippedTop - bottom) / (top - bottom); + + const float uMin = bottomLeft.texCoords[0]; + const float uMax = bottomRight.texCoords[0]; + const float vBottom = bottomLeft.texCoords[1]; + const float vTop = topRight.texCoords[1]; + + const float clippedUMin = Interpolate(uMin, uMax, horizontalRatioLeft); + const float clippedUMax = Interpolate(uMin, uMax, horizontalRatioRight); + const float clippedVBottom = Interpolate(vBottom, vTop, verticalRatioBottom); + const float clippedVTop = Interpolate(vBottom, vTop, verticalRatioTop); + + const uint32_t firstVertex = static_cast(clippedVertices.size()); + auto clippedBottomLeft = p_vertices[quadStart + 0]; + auto clippedBottomRight = p_vertices[quadStart + 1]; + auto clippedTopRight = p_vertices[quadStart + 2]; + auto clippedTopLeft = p_vertices[quadStart + 3]; + + clippedBottomLeft.position[0] = clippedLeft; + clippedBottomLeft.position[1] = clippedBottom; + clippedBottomLeft.texCoords[0] = clippedUMin; + clippedBottomLeft.texCoords[1] = clippedVBottom; + + clippedBottomRight.position[0] = clippedRight; + clippedBottomRight.position[1] = clippedBottom; + clippedBottomRight.texCoords[0] = clippedUMax; + clippedBottomRight.texCoords[1] = clippedVBottom; + + clippedTopRight.position[0] = clippedRight; + clippedTopRight.position[1] = clippedTop; + clippedTopRight.texCoords[0] = clippedUMax; + clippedTopRight.texCoords[1] = clippedVTop; + + clippedTopLeft.position[0] = clippedLeft; + clippedTopLeft.position[1] = clippedTop; + clippedTopLeft.texCoords[0] = clippedUMin; + clippedTopLeft.texCoords[1] = clippedVTop; + + clippedVertices.push_back(clippedBottomLeft); + clippedVertices.push_back(clippedBottomRight); + clippedVertices.push_back(clippedTopRight); + clippedVertices.push_back(clippedTopLeft); + + clippedIndices.push_back(firstVertex + 0); + clippedIndices.push_back(firstVertex + 1); + clippedIndices.push_back(firstVertex + 2); + clippedIndices.push_back(firstVertex + 0); + clippedIndices.push_back(firstVertex + 2); + clippedIndices.push_back(firstVertex + 3); + } + + p_vertices = std::move(clippedVertices); + p_indices = std::move(clippedIndices); + } +} + +OvCore::ECS::Components::UI::TextMeshBuilder::Output OvCore::ECS::Components::UI::TextMeshBuilder::Build(const Input& p_input) +{ + return Build(TextLayoutEngine::Layout(p_input)); +} + +OvCore::ECS::Components::UI::TextMeshBuilder::Output OvCore::ECS::Components::UI::TextMeshBuilder::Build(const TextLayoutEngine::Output& p_layout) +{ + Output output; + output.size = p_layout.size; + + if (p_layout.glyphs.empty()) + { + return output; + } + + std::vector vertices; + std::vector indices; + vertices.reserve(p_layout.glyphs.size() * 4); + indices.reserve(p_layout.glyphs.size() * 6); + + for (const auto& glyph : p_layout.glyphs) + { + const uint32_t firstVertex = static_cast(vertices.size()); + + vertices.push_back({ { glyph.left, glyph.bottom, 0.0f }, { glyph.uMin, glyph.vMax }, { 0.0f, 0.0f, 1.0f }, {}, {} }); + vertices.push_back({ { glyph.right, glyph.bottom, 0.0f }, { glyph.uMax, glyph.vMax }, { 0.0f, 0.0f, 1.0f }, {}, {} }); + vertices.push_back({ { glyph.right, glyph.top, 0.0f }, { glyph.uMax, glyph.vMin }, { 0.0f, 0.0f, 1.0f }, {}, {} }); + vertices.push_back({ { glyph.left, glyph.top, 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); + } + + ClipTextGeometryToSize(vertices, indices, output.size); + if (vertices.empty() || indices.empty()) + { + return output; + } + + output.mesh = std::make_unique(vertices, indices); + return output; +} diff --git a/Sources/OvCore/src/OvCore/ECS/Components/UI/UITransformResolver.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/UITransformResolver.cpp new file mode 100644 index 000000000..387d03e5a --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/UITransformResolver.cpp @@ -0,0 +1,325 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include + +#include +#include +#include +#include + +namespace +{ + constexpr float kDegreesToRadians = 3.14159265359f / 180.0f; + + float KeepFinite(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? p_value : p_fallback; + } + + OvCore::ECS::Actor* FindCanvasOwnerInHierarchy(OvCore::ECS::Actor& p_owner, bool p_includeSelf) + { + auto* current = p_includeSelf ? &p_owner : p_owner.GetParent(); + + while (current) + { + if (current->GetComponent()) + { + return current; + } + + current = current->GetParent(); + } + + return nullptr; + } + + const OvCore::ECS::Actor* FindCanvasOwnerInHierarchy(const OvCore::ECS::Actor& p_owner, bool p_includeSelf) + { + const auto* current = p_includeSelf ? &p_owner : p_owner.GetParent(); + + while (current) + { + if (current->GetComponent()) + { + return current; + } + + current = current->GetParent(); + } + + return nullptr; + } +} + +OvCore::ECS::Components::CTransform::EUIAnchorPreset OvCore::ECS::Components::UI::UITransformResolver::ToAnchorPreset(int p_value) +{ + using EUIAnchorPreset = OvCore::ECS::Components::CTransform::EUIAnchorPreset; + + switch (p_value) + { + case static_cast(EUIAnchorPreset::TOP_LEFT): + return EUIAnchorPreset::TOP_LEFT; + case static_cast(EUIAnchorPreset::TOP_CENTER): + return EUIAnchorPreset::TOP_CENTER; + case static_cast(EUIAnchorPreset::TOP_RIGHT): + return EUIAnchorPreset::TOP_RIGHT; + case static_cast(EUIAnchorPreset::MIDDLE_LEFT): + return EUIAnchorPreset::MIDDLE_LEFT; + case static_cast(EUIAnchorPreset::MIDDLE_RIGHT): + return EUIAnchorPreset::MIDDLE_RIGHT; + case static_cast(EUIAnchorPreset::BOTTOM_LEFT): + return EUIAnchorPreset::BOTTOM_LEFT; + case static_cast(EUIAnchorPreset::BOTTOM_CENTER): + return EUIAnchorPreset::BOTTOM_CENTER; + case static_cast(EUIAnchorPreset::BOTTOM_RIGHT): + return EUIAnchorPreset::BOTTOM_RIGHT; + case static_cast(EUIAnchorPreset::HORIZONTAL_STRETCH_TOP): + return EUIAnchorPreset::HORIZONTAL_STRETCH_TOP; + case static_cast(EUIAnchorPreset::HORIZONTAL_STRETCH_MIDDLE): + return EUIAnchorPreset::HORIZONTAL_STRETCH_MIDDLE; + case static_cast(EUIAnchorPreset::HORIZONTAL_STRETCH_BOTTOM): + return EUIAnchorPreset::HORIZONTAL_STRETCH_BOTTOM; + case static_cast(EUIAnchorPreset::VERTICAL_STRETCH_LEFT): + return EUIAnchorPreset::VERTICAL_STRETCH_LEFT; + case static_cast(EUIAnchorPreset::VERTICAL_STRETCH_CENTER): + return EUIAnchorPreset::VERTICAL_STRETCH_CENTER; + case static_cast(EUIAnchorPreset::VERTICAL_STRETCH_RIGHT): + return EUIAnchorPreset::VERTICAL_STRETCH_RIGHT; + case static_cast(EUIAnchorPreset::STRETCH_BOTH): + return EUIAnchorPreset::STRETCH_BOTH; + case static_cast(EUIAnchorPreset::CENTER): + default: + return EUIAnchorPreset::CENTER; + } +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::UITransformResolver::GetAnchorRatio(OvCore::ECS::Components::CTransform::EUIAnchorPreset p_anchorPreset) +{ + using EUIAnchorPreset = OvCore::ECS::Components::CTransform::EUIAnchorPreset; + + switch (p_anchorPreset) + { + case EUIAnchorPreset::TOP_LEFT: + return { -0.5f, 0.5f }; + case EUIAnchorPreset::TOP_CENTER: + return { 0.0f, 0.5f }; + case EUIAnchorPreset::TOP_RIGHT: + return { 0.5f, 0.5f }; + case EUIAnchorPreset::MIDDLE_LEFT: + return { -0.5f, 0.0f }; + case EUIAnchorPreset::MIDDLE_RIGHT: + return { 0.5f, 0.0f }; + case EUIAnchorPreset::BOTTOM_LEFT: + return { -0.5f, -0.5f }; + case EUIAnchorPreset::BOTTOM_CENTER: + return { 0.0f, -0.5f }; + case EUIAnchorPreset::BOTTOM_RIGHT: + return { 0.5f, -0.5f }; + case EUIAnchorPreset::HORIZONTAL_STRETCH_TOP: + return { 0.0f, 0.5f }; + case EUIAnchorPreset::HORIZONTAL_STRETCH_MIDDLE: + return { 0.0f, 0.0f }; + case EUIAnchorPreset::HORIZONTAL_STRETCH_BOTTOM: + return { 0.0f, -0.5f }; + case EUIAnchorPreset::VERTICAL_STRETCH_LEFT: + return { -0.5f, 0.0f }; + case EUIAnchorPreset::VERTICAL_STRETCH_CENTER: + return { 0.0f, 0.0f }; + case EUIAnchorPreset::VERTICAL_STRETCH_RIGHT: + return { 0.5f, 0.0f }; + case EUIAnchorPreset::STRETCH_BOTH: + return { 0.0f, 0.0f }; + case EUIAnchorPreset::CENTER: + default: + return { 0.0f, 0.0f }; + } +} + +bool OvCore::ECS::Components::UI::UITransformResolver::IsHorizontalPositionEditable(OvCore::ECS::Components::CTransform::EUIAnchorPreset p_anchorPreset) +{ + using EUIAnchorPreset = OvCore::ECS::Components::CTransform::EUIAnchorPreset; + + switch (p_anchorPreset) + { + case EUIAnchorPreset::HORIZONTAL_STRETCH_TOP: + case EUIAnchorPreset::HORIZONTAL_STRETCH_MIDDLE: + case EUIAnchorPreset::HORIZONTAL_STRETCH_BOTTOM: + case EUIAnchorPreset::STRETCH_BOTH: + return false; + default: + return true; + } +} + +bool OvCore::ECS::Components::UI::UITransformResolver::IsVerticalPositionEditable(OvCore::ECS::Components::CTransform::EUIAnchorPreset p_anchorPreset) +{ + using EUIAnchorPreset = OvCore::ECS::Components::CTransform::EUIAnchorPreset; + + switch (p_anchorPreset) + { + case EUIAnchorPreset::VERTICAL_STRETCH_LEFT: + case EUIAnchorPreset::VERTICAL_STRETCH_CENTER: + case EUIAnchorPreset::VERTICAL_STRETCH_RIGHT: + case EUIAnchorPreset::STRETCH_BOTH: + return false; + default: + return true; + } +} + +OvCore::ECS::Actor* OvCore::ECS::Components::UI::UITransformResolver::FindCanvasOwner(ECS::Actor& p_owner) +{ + return FindCanvasOwnerInHierarchy(p_owner, true); +} + +const OvCore::ECS::Actor* OvCore::ECS::Components::UI::UITransformResolver::FindCanvasOwner(const ECS::Actor& p_owner) +{ + return FindCanvasOwnerInHierarchy(p_owner, true); +} + +const OvCore::ECS::Actor* OvCore::ECS::Components::UI::UITransformResolver::FindActiveCanvasOwner(const ECS::Actor& p_owner) +{ + return FindCanvasOwnerInHierarchy(p_owner, false); +} + +bool OvCore::ECS::Components::UI::UITransformResolver::HasActiveUIData(const ECS::Actor& p_owner) +{ + return FindActiveCanvasOwner(p_owner) != nullptr; +} + +bool OvCore::ECS::Components::UI::UITransformResolver::IsDrivenByLayout(const ECS::Actor& p_owner) +{ + const auto* parent = p_owner.GetParent(); + return parent && parent->GetComponent(); +} + +OvCore::ECS::Components::UI::UITransformResolver::LayoutData OvCore::ECS::Components::UI::UITransformResolver::ResolveLayoutData(const ECS::Actor& p_owner) +{ + LayoutData result; + const auto* child = &p_owner; + + while (const auto* parent = child->GetParent()) + { + if (const auto* layout = parent->GetComponent()) + { + if (const auto childLayout = layout->GetChildLayout(*child); childLayout && childLayout->valid) + { + result.offset += childLayout->offset; + + if (child == &p_owner) + { + if (childLayout->hasDirectWidth && childLayout->size.x > 0.0f) + { + result.directSize.x = childLayout->size.x; + result.hasDirectWidth = true; + } + + if (childLayout->hasDirectHeight && childLayout->size.y > 0.0f) + { + result.directSize.y = childLayout->size.y; + result.hasDirectHeight = true; + } + } + } + } + + child = parent; + } + + return result; +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::UITransformResolver::GetEffectiveSize( + const OvCore::ECS::Components::CTransform& p_transform, + const OvMaths::FVector2& p_elementSize +) +{ + const auto& size = p_transform.GetUISize(); + return { + size.x > 0.0f ? size.x : std::max(p_elementSize.x, 0.0f), + size.y > 0.0f ? size.y : std::max(p_elementSize.y, 0.0f) + }; +} + +OvMaths::FVector2 OvCore::ECS::Components::UI::UITransformResolver::GetAnchoredPosition( + const OvCore::ECS::Components::CTransform& p_transform, + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset +) +{ + if (IsDrivenByLayout(p_transform.owner)) + { + const auto* parent = p_transform.owner.GetParent(); + if (!parent) + { + return p_layoutOffset; + } + + if (const auto* parentLayout = parent->GetComponent()) + { + const auto childLayoutOffset = parentLayout->GetChildOffset(p_transform.owner); + const auto parentLayoutOffset = p_layoutOffset - childLayoutOffset; + const auto parentAnchoredPosition = GetAnchoredPosition(parent->transform, p_canvasSize, parentLayoutOffset); + + return parentAnchoredPosition + childLayoutOffset; + } + + return p_layoutOffset; + } + + const auto anchorRatio = GetAnchorRatio(p_transform.GetUIAnchorPreset()); + const OvMaths::FVector2 anchorOffset = { + KeepFinite(p_canvasSize.x, 0.0f) * anchorRatio.x, + KeepFinite(p_canvasSize.y, 0.0f) * anchorRatio.y + }; + const float positionX = IsHorizontalPositionEditable(p_transform.GetUIAnchorPreset()) ? p_transform.GetUIPosition().x : 0.0f; + const float positionY = IsVerticalPositionEditable(p_transform.GetUIAnchorPreset()) ? p_transform.GetUIPosition().y : 0.0f; + + return { + anchorOffset.x + p_layoutOffset.x + positionX, + anchorOffset.y + p_layoutOffset.y + positionY + }; +} + +OvMaths::FMatrix4 OvCore::ECS::Components::UI::UITransformResolver::GetMatrix( + const OvCore::ECS::Components::CTransform& p_transform, + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset, + const OvMaths::FVector2& p_elementSize +) +{ + return GetMatrixWithEffectiveSize( + p_transform, + p_canvasSize, + p_layoutOffset, + GetEffectiveSize(p_transform, p_elementSize) + ); +} + +OvMaths::FMatrix4 OvCore::ECS::Components::UI::UITransformResolver::GetMatrixWithEffectiveSize( + const OvCore::ECS::Components::CTransform& p_transform, + const OvMaths::FVector2& p_canvasSize, + const OvMaths::FVector2& p_layoutOffset, + const OvMaths::FVector2& p_effectiveSize +) +{ + const auto position = GetAnchoredPosition(p_transform, p_canvasSize, p_layoutOffset); + const auto scale = p_transform.GetUIScale(); + const auto halfSize = p_effectiveSize * 0.5f; + const auto& pivot = p_transform.GetUIPivot(); + const OvMaths::FVector2 pivotOffset = { + -pivot.x * halfSize.x, + pivot.y * halfSize.y + }; + + return + OvMaths::FMatrix4::Translation({ position.x, position.y, 0.0f }) * + OvMaths::FMatrix4::RotationOnAxisZ(p_transform.GetUIRotation() * kDegreesToRadians) * + OvMaths::FMatrix4::Scaling({ scale.x, scale.y, 1.0f }) * + OvMaths::FMatrix4::Translation({ pivotOffset.x, pivotOffset.y, 0.0f }); +} diff --git a/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp b/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp index cd4840c0c..d1521733f 100644 --- a/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp +++ b/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp @@ -13,6 +13,7 @@ namespace OvRendering::Resources::Texture* __EMPTY_TEXTURE = nullptr; OvCore::Helpers::GUIHelpers::FileItemBuilderCallback __FILE_ITEM_BUILDER; OvCore::Helpers::GUIHelpers::PickerProviderCallback __PICKER_PROVIDER; + OvCore::Helpers::GUIHelpers::PickerCloseProviderCallback __PICKER_CLOSE_PROVIDER; OvCore::Helpers::GUIHelpers::PickerSearchTextProviderCallback __PICKER_SEARCH_TEXT_PROVIDER; OvCore::Helpers::GUIHelpers::IconProviderCallback __ICON_PROVIDER; OvCore::Helpers::GUIHelpers::OpenProviderCallback __OPEN_PROVIDER; @@ -105,6 +106,17 @@ void OvCore::Helpers::GUIHelpers::OpenPicker(PickerItemList p_items, std::string __PICKER_PROVIDER(std::move(p_items), std::move(p_title)); } +void OvCore::Helpers::GUIHelpers::SetPickerCloseProvider(PickerCloseProviderCallback p_provider) +{ + __PICKER_CLOSE_PROVIDER = std::move(p_provider); +} + +void OvCore::Helpers::GUIHelpers::ClosePicker() +{ + if (__PICKER_CLOSE_PROVIDER) + __PICKER_CLOSE_PROVIDER(); +} + void OvCore::Helpers::GUIHelpers::SetPickerSearchTextProvider(PickerSearchTextProviderCallback p_provider) { __PICKER_SEARCH_TEXT_PROVIDER = std::move(p_provider); 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..166a0513b 100644 --- a/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp @@ -4,13 +4,18 @@ * @licence: MIT */ +#include #include #include +#include #include #include #include #include +#include +#include +#include #include #include #include @@ -23,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -163,6 +169,211 @@ namespace } return probes; } + + EngineDrawableDescriptor CreateUIDrawableDescriptor( + OvCore::ECS::Actor& p_owner, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver, + const OvMaths::FMatrix4& p_uiProjectionMatrix, + const OvMaths::FVector2& p_elementSize + ) + { + EngineDrawableDescriptor descriptor{ + .modelMatrix = p_owner.transform.GetFTransform().GetWorldMatrix(), + .userMatrix = OvMaths::FMatrix4::Identity + }; + + OvCore::Rendering::UIRenderingUtils::ResolvedUIElement resolvedElement; + if (p_uiFrameResolver.ResolveElement( + p_owner, + p_elementSize, + resolvedElement + )) + { + descriptor.modelMatrix = resolvedElement.modelMatrix; + + if (p_uiFrameResolver.IsScreenSpace()) + { + descriptor.viewMatrixOverride = OvMaths::FMatrix4::Identity; + descriptor.projectionMatrixOverride = p_uiProjectionMatrix; + } + } + + return descriptor; + } + + void AppendImageDrawable( + SceneRenderer::SceneDrawablesDescriptor& p_result, + OvCore::ECS::Components::UI::CImage& p_image, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver, + const OvMaths::FMatrix4& p_uiProjectionMatrix, + 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_uiFrameResolver, + p_uiProjectionMatrix, + p_image.GetIntrinsicSize() + ) + ); + + p_result.drawables.push_back(drawable); + } + + void AppendTextDrawable( + SceneRenderer::SceneDrawablesDescriptor& p_result, + OvCore::ECS::Components::UI::CText& p_text, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver, + const OvMaths::FMatrix4& p_uiProjectionMatrix, + int p_drawOrder + ) + { + auto& owner = p_text.owner; + auto* material = p_text.GetMaterial(); + if (!material) return; + + const auto baseTextSize = p_text.GetSize(); + OvCore::Rendering::UIRenderingUtils::ResolvedUIElement resolvedElement; + const bool hasResolvedElement = p_uiFrameResolver.ResolveElement( + owner, + baseTextSize, + resolvedElement + ); + const auto resolvedTextSize = hasResolvedElement ? resolvedElement.effectiveSize : baseTextSize; + + auto* mesh = p_text.GetMesh(resolvedTextSize); + if (!mesh) return; + const auto renderedTextSize = p_text.GetSize(resolvedTextSize); + + 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_uiFrameResolver, + p_uiProjectionMatrix, + renderedTextSize + ) + ); + + p_result.drawables.push_back(drawable); + } + + void AppendHierarchyUIDrawables( + SceneRenderer::SceneDrawablesDescriptor& p_result, + OvCore::ECS::Actor& p_actor, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver, + const OvCore::ECS::Components::UI::CCanvas* p_canvas, + const OvMaths::FMatrix4& p_uiProjectionMatrix, + int& p_drawOrder + ) + { + if (!p_actor.IsActive()) + { + return; + } + + if (auto* canvas = p_actor.GetComponent()) + { + p_canvas = canvas; + } + + if (p_canvas) + { + if (auto* image = p_actor.GetComponent()) + { + AppendImageDrawable( + p_result, + *image, + p_uiFrameResolver, + p_uiProjectionMatrix, + p_drawOrder++ + ); + } + + if (auto* text = p_actor.GetComponent()) + { + AppendTextDrawable( + p_result, + *text, + p_uiFrameResolver, + p_uiProjectionMatrix, + p_drawOrder++ + ); + } + } + + for (auto* child : p_actor.GetChildren()) + { + if (child) + { + AppendHierarchyUIDrawables( + p_result, + *child, + p_uiFrameResolver, + p_canvas, + p_uiProjectionMatrix, + p_drawOrder + ); + } + } + } + + void AppendHierarchyUIDrawables( + SceneRenderer::SceneDrawablesDescriptor& p_result, + OvCore::SceneSystem::Scene& p_scene, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver + ) + { + int drawOrder = 0; + const auto uiProjectionMatrix = p_uiFrameResolver.CreateProjectionMatrix(); + + for (auto* actor : p_scene.GetActors()) + { + if (actor && !actor->HasParent()) + { + AppendHierarchyUIDrawables( + p_result, + *actor, + p_uiFrameResolver, + nullptr, + uiProjectionMatrix, + drawOrder + ); + } + } + } } OvCore::Rendering::SceneRenderer::SceneRenderer(OvRendering::Context::Driver& p_driver, bool p_stencilWrite) @@ -200,6 +411,10 @@ void OvCore::Rendering::SceneRenderer::BeginFrame(const OvRendering::Data::Frame OVASSERT(HasDescriptor(), "Cannot find SceneDescriptor attached to this renderer"); auto& sceneDescriptor = GetDescriptor(); + const auto renderSize = OvMaths::FVector2{ + static_cast(p_frameDescriptor.renderWidth), + static_cast(p_frameDescriptor.renderHeight) + }; const bool frustumLightCulling = p_frameDescriptor.camera.value().HasFrustumLightCulling(); @@ -212,16 +427,24 @@ void OvCore::Rendering::SceneRenderer::BeginFrame(const OvRendering::Data::Frame FindActiveReflectionProbes(sceneDescriptor.scene) }); + SetDescriptor(OvCore::Rendering::UIRenderingUtils::UIFrameResolver{ + renderSize, + sceneDescriptor.renderUIInScreenSpace + }); + OvRendering::Core::CompositeRenderer::BeginFrame(p_frameDescriptor); AddDescriptor({ ParseScene(SceneParsingInput{ - .scene = sceneDescriptor.scene + .scene = sceneDescriptor.scene, + .renderSize = renderSize, + .renderUIInScreenSpace = sceneDescriptor.renderUIInScreenSpace, + .uiFrameResolver = &GetDescriptor() }) }); // 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 +454,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 +492,14 @@ 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; + OvCore::Rendering::UIRenderingUtils::UIFrameResolver fallbackUIFrameResolver{ + p_input.renderSize, + p_input.renderUIInScreenSpace + }; + const auto& uiFrameResolver = p_input.uiFrameResolver ? + *p_input.uiFrameResolver : + fallbackUIFrameResolver; for (const auto modelRenderer : scene.GetFastAccessComponents().modelRenderers) { @@ -316,7 +547,7 @@ SceneRenderer::SceneDrawablesDescriptor OvCore::Rendering::SceneRenderer::ParseS .visibilityFlags = materialRenderer->GetVisibilityFlags(), .bounds = bounds }); - + drawable.AddDescriptor({ transform.GetWorldMatrix(), materialRenderer->GetUserMatrix() @@ -331,6 +562,8 @@ SceneRenderer::SceneDrawablesDescriptor OvCore::Rendering::SceneRenderer::ParseS } } + AppendHierarchyUIDrawables(result, scene, uiFrameResolver); + return result; } @@ -368,7 +601,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 +667,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..94671335a --- /dev/null +++ b/Sources/OvCore/src/OvCore/Rendering/UIRenderingUtils.cpp @@ -0,0 +1,645 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr float kDegreesToRadians = 3.14159265359f / 180.0f; + 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; + } + + float KeepFinite(float p_value, float p_fallback) + { + return std::isfinite(p_value) ? p_value : p_fallback; + } + + OvMaths::FMatrix4 CalculateUnscaledModelMatrix(const OvCore::ECS::Actor& p_actor) + { + return + OvMaths::FMatrix4::Translation(p_actor.transform.GetWorldPosition()) * + OvMaths::FQuaternion::ToMatrix4(p_actor.transform.GetWorldRotation()); + } + + OvMaths::FVector2 GetLocalAnchoredPosition( + const OvCore::ECS::Components::CTransform& p_transform, + const OvMaths::FVector2& p_parentSize, + const OvMaths::FVector2& p_layoutOffset, + bool p_drivenByLayout + ) + { + if (p_drivenByLayout) + { + return p_layoutOffset; + } + + const auto anchorPreset = p_transform.GetUIAnchorPreset(); + const auto anchorRatio = OvCore::ECS::Components::UI::UITransformResolver::GetAnchorRatio(anchorPreset); + const OvMaths::FVector2 anchorOffset = { + KeepFinite(p_parentSize.x, 0.0f) * anchorRatio.x, + KeepFinite(p_parentSize.y, 0.0f) * anchorRatio.y + }; + const float positionX = OvCore::ECS::Components::UI::UITransformResolver::IsHorizontalPositionEditable(anchorPreset) ? + p_transform.GetUIPosition().x : + 0.0f; + const float positionY = OvCore::ECS::Components::UI::UITransformResolver::IsVerticalPositionEditable(anchorPreset) ? + p_transform.GetUIPosition().y : + 0.0f; + + return { + anchorOffset.x + p_layoutOffset.x + positionX, + anchorOffset.y + p_layoutOffset.y + positionY + }; + } + + OvMaths::FMatrix4 CreateUIElementFrameMatrix( + const OvCore::ECS::Components::CTransform& p_transform, + const OvMaths::FVector2& p_parentSize, + const OvMaths::FVector2& p_layoutOffset, + const OvMaths::FVector2& p_effectiveSize, + bool p_drivenByLayout + ) + { + const auto position = GetLocalAnchoredPosition( + p_transform, + p_parentSize, + p_layoutOffset, + p_drivenByLayout + ); + const auto scale = p_transform.GetUIScale(); + const auto halfSize = p_effectiveSize * 0.5f; + const auto& pivot = p_transform.GetUIPivot(); + const OvMaths::FVector2 pivotOffset = { + -pivot.x * halfSize.x, + pivot.y * halfSize.y + }; + + return + OvMaths::FMatrix4::Translation({ position.x, position.y, 0.0f }) * + OvMaths::FMatrix4::RotationOnAxisZ(p_transform.GetUIRotation() * kDegreesToRadians) * + OvMaths::FMatrix4::Scaling({ scale.x, scale.y, 1.0f }) * + OvMaths::FMatrix4::Translation({ pivotOffset.x, pivotOffset.y, 0.0f }); + } + + OvMaths::FMatrix4 ApplyElementSizeScaling( + OvMaths::FMatrix4 p_matrix, + const OvMaths::FVector2& p_elementSize, + const OvMaths::FVector2& p_effectiveSize + ) + { + if (p_elementSize.x > 0.0f || p_elementSize.y > 0.0f) + { + p_matrix = p_matrix * OvMaths::FMatrix4::Scaling({ + p_elementSize.x > 0.0f ? p_effectiveSize.x / p_elementSize.x : 1.0f, + p_elementSize.y > 0.0f ? p_effectiveSize.y / p_elementSize.y : 1.0f, + 1.0f + }); + } + + return p_matrix; + } +} + +OvCore::Rendering::UIRenderingUtils::UIFrameResolver::UIFrameResolver( + const OvMaths::FVector2& p_renderSize, + bool p_screenSpace +) : +m_renderSize(ClampCanvasSize(p_renderSize)), +m_screenSpace(p_screenSpace) +{ +} + +const OvMaths::FVector2& OvCore::Rendering::UIRenderingUtils::UIFrameResolver::GetRenderSize() const +{ + return m_renderSize; +} + +bool OvCore::Rendering::UIRenderingUtils::UIFrameResolver::IsScreenSpace() const +{ + return m_screenSpace; +} + +OvMaths::FMatrix4 OvCore::Rendering::UIRenderingUtils::UIFrameResolver::CreateProjectionMatrix( + float p_near, + float p_far +) const +{ + return CreateUIProjectionMatrix(m_renderSize, p_near, p_far); +} + +OvMaths::FVector2 OvCore::Rendering::UIRenderingUtils::UIFrameResolver::GetElementSize(const OvCore::ECS::Actor& p_actor) const +{ + if (const auto it = m_elementSizeCache.find(&p_actor); it != m_elementSizeCache.end()) + { + return it->second; + } + + const auto elementSize = UIRenderingUtils::GetElementSize(p_actor, m_renderSize); + m_elementSizeCache.emplace(&p_actor, elementSize); + return elementSize; +} + +bool OvCore::Rendering::UIRenderingUtils::UIFrameResolver::ResolveCanvas( + const OvCore::ECS::Actor& p_actor, + ResolvedUICanvas& p_outCanvas +) const +{ + if (const auto it = m_canvasCache.find(&p_actor); it != m_canvasCache.end()) + { + if (!it->second) + { + return false; + } + + p_outCanvas = it->second.value(); + return true; + } + + ResolvedUICanvas resolvedCanvas; + if (!ResolveCanvasUncached(p_actor, resolvedCanvas)) + { + m_canvasCache.emplace(&p_actor, std::nullopt); + return false; + } + + m_canvasCache.emplace(&p_actor, resolvedCanvas); + p_outCanvas = resolvedCanvas; + return true; +} + +bool OvCore::Rendering::UIRenderingUtils::UIFrameResolver::ResolveElement( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_elementSize, + ResolvedUIElement& p_outElement +) const +{ + const ElementKey key{ + .actor = &p_actor, + .width = p_elementSize.x, + .height = p_elementSize.y + }; + + if (const auto it = m_elementCache.find(key); it != m_elementCache.end()) + { + if (!it->second) + { + return false; + } + + p_outElement = it->second.value(); + return true; + } + + ResolvedUIElement resolvedElement; + if (!ResolveElementUncached(p_actor, p_elementSize, resolvedElement)) + { + m_elementCache.emplace(key, std::nullopt); + return false; + } + + m_elementCache.emplace(key, resolvedElement); + p_outElement = resolvedElement; + return true; +} + +bool OvCore::Rendering::UIRenderingUtils::UIFrameResolver::ResolveElement( + const OvCore::ECS::Actor& p_actor, + ResolvedUIElement& p_outElement +) const +{ + return ResolveElement(p_actor, GetElementSize(p_actor), p_outElement); +} + +bool OvCore::Rendering::UIRenderingUtils::UIFrameResolver::ElementKey::operator==(const ElementKey& p_other) const +{ + return actor == p_other.actor && width == p_other.width && height == p_other.height; +} + +std::size_t OvCore::Rendering::UIRenderingUtils::UIFrameResolver::ElementKeyHash::operator()(const ElementKey& p_key) const +{ + const auto actorHash = std::hash{}(p_key.actor); + const auto widthHash = std::hash{}(p_key.width); + const auto heightHash = std::hash{}(p_key.height); + return actorHash ^ (widthHash << 1) ^ (heightHash << 2); +} + +bool OvCore::Rendering::UIRenderingUtils::UIFrameResolver::ResolveCanvasUncached( + const OvCore::ECS::Actor& p_actor, + ResolvedUICanvas& p_outCanvas +) const +{ + const auto* canvas = p_actor.GetComponent(); + if (!canvas) + { + return false; + } + + p_outCanvas.actor = &p_actor; + p_outCanvas.canvas = canvas; + p_outCanvas.size = UIRenderingUtils::GetCanvasSize(*canvas, m_renderSize); + p_outCanvas.matrix = m_screenSpace ? OvMaths::FMatrix4::Identity : CalculateUnscaledModelMatrix(p_actor); + p_outCanvas.canvasScale = UIRenderingUtils::GetCanvasScale(*canvas, m_renderSize); + p_outCanvas.worldScale = UIRenderingUtils::GetUIWorldScale(*canvas, m_screenSpace); + p_outCanvas.unitsScale = m_screenSpace ? p_outCanvas.canvasScale : p_outCanvas.canvasScale * p_outCanvas.worldScale; + p_outCanvas.modelMatrix = + p_outCanvas.matrix * + OvMaths::FMatrix4::Scaling({ p_outCanvas.unitsScale, p_outCanvas.unitsScale, 1.0f }); + p_outCanvas.screenSpace = m_screenSpace; + + return p_outCanvas.size.x > 0.0f && p_outCanvas.size.y > 0.0f; +} + +bool OvCore::Rendering::UIRenderingUtils::UIFrameResolver::ResolveElementUncached( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_elementSize, + ResolvedUIElement& p_outElement +) const +{ + if (!HasActiveUIData(p_actor)) + { + return false; + } + + const auto* canvasOwner = FindCanvasOwner(p_actor); + if (!canvasOwner) + { + return false; + } + + ResolvedUICanvas resolvedCanvas; + if (!ResolveCanvas(*canvasOwner, resolvedCanvas)) + { + return false; + } + + const auto& transform = p_actor.transform; + const auto layoutData = GetLayoutData(p_actor); + OvMaths::FMatrix4 parentFrameMatrix = resolvedCanvas.modelMatrix; + OvMaths::FVector2 parentSize = resolvedCanvas.size; + + const auto* parent = p_actor.GetParent(); + if (parent && parent != canvasOwner && HasActiveUIData(*parent)) + { + ResolvedUIElement resolvedParent; + if (!ResolveElement(*parent, resolvedParent)) + { + return false; + } + + parentFrameMatrix = resolvedParent.frameMatrix; + parentSize = resolvedParent.effectiveSize; + } + + p_outElement.actor = &p_actor; + p_outElement.canvasActor = canvasOwner; + p_outElement.canvas = resolvedCanvas.canvas; + p_outElement.canvasSize = resolvedCanvas.size; + p_outElement.layoutOffset = layoutData.offset; + p_outElement.elementSize = p_elementSize; + p_outElement.effectiveSize = OvCore::ECS::Components::UI::UITransformResolver::GetEffectiveSize(transform, p_elementSize); + if (layoutData.hasDirectWidth) + { + p_outElement.effectiveSize.x = layoutData.directSize.x; + } + if (layoutData.hasDirectHeight) + { + p_outElement.effectiveSize.y = layoutData.directSize.y; + } + p_outElement.canvasMatrix = resolvedCanvas.matrix; + const auto localFrameMatrix = CreateUIElementFrameMatrix( + transform, + parentSize, + p_outElement.layoutOffset, + p_outElement.effectiveSize, + layoutData.drivenByLayout + ); + p_outElement.localMatrix = ApplyElementSizeScaling( + localFrameMatrix, + p_elementSize, + p_outElement.effectiveSize + ); + p_outElement.canvasScale = resolvedCanvas.canvasScale; + p_outElement.worldScale = resolvedCanvas.worldScale; + p_outElement.unitsScale = resolvedCanvas.unitsScale; + p_outElement.frameMatrix = parentFrameMatrix * localFrameMatrix; + p_outElement.modelMatrix = parentFrameMatrix * p_outElement.localMatrix; + p_outElement.screenSpace = m_screenSpace; + + return true; +} + +bool OvCore::Rendering::UIRenderingUtils::UIFrameResolver::HasActiveUIData(const OvCore::ECS::Actor& p_actor) const +{ + if (const auto it = m_activeUIDataCache.find(&p_actor); it != m_activeUIDataCache.end()) + { + return it->second; + } + + const bool hasActiveUIData = OvCore::ECS::Components::UI::UITransformResolver::HasActiveUIData(p_actor); + m_activeUIDataCache.emplace(&p_actor, hasActiveUIData); + return hasActiveUIData; +} + +const OvCore::ECS::Actor* OvCore::Rendering::UIRenderingUtils::UIFrameResolver::FindCanvasOwner(const OvCore::ECS::Actor& p_actor) const +{ + if (const auto it = m_canvasOwnerCache.find(&p_actor); it != m_canvasOwnerCache.end()) + { + return it->second; + } + + const auto* canvasOwner = UIRenderingUtils::FindCanvasOwner(p_actor); + m_canvasOwnerCache.emplace(&p_actor, canvasOwner); + return canvasOwner; +} + +OvCore::Rendering::UIRenderingUtils::UIFrameResolver::CachedLayoutData OvCore::Rendering::UIRenderingUtils::UIFrameResolver::GetLayoutData(const OvCore::ECS::Actor& p_actor) const +{ + if (const auto it = m_layoutDataCache.find(&p_actor); it != m_layoutDataCache.end()) + { + return it->second; + } + + CachedLayoutData cachedLayoutData; + const auto* parent = p_actor.GetParent(); + if (parent) + { + if (const auto* layout = parent->GetComponent()) + { + cachedLayoutData.drivenByLayout = true; + + if (const auto childLayout = layout->GetChildLayout(p_actor); childLayout && childLayout->valid) + { + cachedLayoutData.offset = childLayout->offset; + + if (childLayout->hasDirectWidth && childLayout->size.x > 0.0f) + { + cachedLayoutData.directSize.x = childLayout->size.x; + cachedLayoutData.hasDirectWidth = true; + } + + if (childLayout->hasDirectHeight && childLayout->size.y > 0.0f) + { + cachedLayoutData.directSize.y = childLayout->size.y; + cachedLayoutData.hasDirectHeight = true; + } + } + } + } + + m_layoutDataCache.emplace(&p_actor, cachedLayoutData); + return cachedLayoutData; +} + +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) + }; +} + +OvMaths::FMatrix4 OvCore::Rendering::UIRenderingUtils::CreateUIProjectionMatrix( + const OvMaths::FVector2& p_renderSize, + float p_near, + float p_far +) +{ + const auto renderSize = ClampCanvasSize(p_renderSize); + const auto aspectRatio = renderSize.x / renderSize.y; + + return OvMaths::FMatrix4::CreateOrthographic(renderSize.y * 0.5f, aspectRatio, p_near, p_far); +} + +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) +{ + if (const auto* canvasOwner = FindCanvasOwner(p_owner)) + { + return canvasOwner->GetComponent(); + } + + return nullptr; +} + +OvCore::ECS::Actor* OvCore::Rendering::UIRenderingUtils::FindCanvasOwner(OvCore::ECS::Actor& p_owner) +{ + return OvCore::ECS::Components::UI::UITransformResolver::FindCanvasOwner(p_owner); +} + +const OvCore::ECS::Actor* OvCore::Rendering::UIRenderingUtils::FindCanvasOwner(const OvCore::ECS::Actor& p_owner) +{ + return OvCore::ECS::Components::UI::UITransformResolver::FindCanvasOwner(p_owner); +} + +OvMaths::FMatrix4 OvCore::Rendering::UIRenderingUtils::GetCanvasMatrix( + const OvCore::ECS::Actor& p_owner, + bool p_screenSpace +) +{ + if (p_screenSpace) + { + return OvMaths::FMatrix4::Identity; + } + + if (const auto* canvasOwner = FindCanvasOwner(p_owner)) + { + return CalculateUnscaledModelMatrix(*canvasOwner); + } + + return OvMaths::FMatrix4::Identity; +} + +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::GetElementSize( + const OvCore::ECS::Actor& p_owner, + const OvMaths::FVector2& p_renderSize +) +{ + if (const auto* image = p_owner.GetComponent()) + { + return image->GetIntrinsicSize(); + } + + if (const auto* text = p_owner.GetComponent()) + { + return text->GetSize(); + } + + if (const auto* layout = p_owner.GetComponent()) + { + return layout->GetComputedSize(); + } + + if (const auto* canvas = p_owner.GetComponent()) + { + return GetCanvasSize(*canvas, p_renderSize); + } + + return p_owner.transform.GetUISize(); +} + +OvMaths::FVector2 OvCore::Rendering::UIRenderingUtils::GetLayoutOffset(const OvCore::ECS::Actor& p_owner) +{ + return OvCore::ECS::Components::UI::UITransformResolver::ResolveLayoutData(p_owner).offset; +} + +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(); +} + +OvMaths::FVector3 OvCore::Rendering::UIRenderingUtils::TransformUIPoint( + 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::FVector3 OvCore::Rendering::UIRenderingUtils::TransformUIElementPivot(const ResolvedUIElement& p_element) +{ + if (!p_element.actor) + { + return TransformUIPoint(p_element.modelMatrix, OvMaths::FVector2::Zero); + } + + const auto& pivot = p_element.actor->transform.GetUIPivot(); + const OvMaths::FVector2 referenceSize = { + p_element.elementSize.x > 0.0f ? p_element.elementSize.x : p_element.effectiveSize.x, + p_element.elementSize.y > 0.0f ? p_element.elementSize.y : p_element.effectiveSize.y + }; + + return TransformUIPoint( + p_element.modelMatrix, + { + pivot.x * referenceSize.x * 0.5f, + -pivot.y * referenceSize.y * 0.5f + } + ); +} + +bool OvCore::Rendering::UIRenderingUtils::ResolveUICanvas( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_renderSize, + bool p_screenSpace, + ResolvedUICanvas& p_outCanvas +) +{ + return UIFrameResolver(p_renderSize, p_screenSpace).ResolveCanvas(p_actor, p_outCanvas); +} + +bool OvCore::Rendering::UIRenderingUtils::ResolveUIElement( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_renderSize, + bool p_screenSpace, + const OvMaths::FVector2& p_elementSize, + ResolvedUIElement& p_outElement +) +{ + return UIFrameResolver(p_renderSize, p_screenSpace).ResolveElement(p_actor, p_elementSize, p_outElement); +} + +bool OvCore::Rendering::UIRenderingUtils::ResolveUIElement( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_renderSize, + bool p_screenSpace, + ResolvedUIElement& p_outElement +) +{ + return UIFrameResolver(p_renderSize, p_screenSpace).ResolveElement(p_actor, p_outElement); +} 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/ResourceManagement/UIResourceRegistry.cpp b/Sources/OvCore/src/OvCore/ResourceManagement/UIResourceRegistry.cpp new file mode 100644 index 000000000..15f5ea967 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ResourceManagement/UIResourceRegistry.cpp @@ -0,0 +1,19 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include + +#include + +void OvCore::ResourceManagement::UIResourceRegistry::ProvideDefinition(Definition p_definition) +{ + m_definition = std::move(p_definition); +} + +const OvCore::ResourceManagement::UIResourceRegistry::Definition& OvCore::ResourceManagement::UIResourceRegistry::GetDefinition() const +{ + return m_definition; +} diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp index bc3ec5e11..7e06320a4 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp @@ -26,6 +26,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include void BindLuaActor(sol::state& p_luaState) @@ -72,6 +78,12 @@ 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, + "GetVerticalLayout", &Actor::GetComponent, /* Behaviours relatives */ "GetBehaviour", [](Actor& p_this, const std::string& p_name) -> sol::table { @@ -125,6 +137,12 @@ 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, + "AddVerticalLayout", &Actor::AddComponent, /* Components Destructors */ "RemoveModelRenderer", &Actor::RemoveComponent, @@ -143,6 +161,12 @@ 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, + "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..cd0eea958 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp @@ -21,9 +21,15 @@ #include #include #include -#include -#include +#include +#include #include +#include +#include +#include +#include +#include +#include void BindLuaComponents(sol::state& p_luaState) { @@ -63,7 +69,23 @@ void BindLuaComponents(sol::state& p_luaState) "GetLocalRight", &CTransform::GetLocalRight, "GetWorldForward", &CTransform::GetWorldForward, "GetWorldUp", &CTransform::GetWorldUp, - "GetWorldRight", &CTransform::GetWorldRight + "GetWorldRight", &CTransform::GetWorldRight, + "EnableUIData", &CTransform::EnableUIData, + "DisableUIData", &CTransform::DisableUIData, + "HasUIData", &CTransform::HasUIData, + "HasActiveUIData", &CTransform::HasActiveUIData, + "GetUIPosition", [](CTransform& p_this) -> FVector2 { return p_this.GetUIPosition(); }, + "SetUIPosition", &CTransform::SetUIPosition, + "GetUIRotation", &CTransform::GetUIRotation, + "SetUIRotation", &CTransform::SetUIRotation, + "GetUIScale", [](CTransform& p_this) -> FVector2 { return p_this.GetUIScale(); }, + "SetUIScale", &CTransform::SetUIScale, + "GetUISize", [](CTransform& p_this) -> FVector2 { return p_this.GetUISize(); }, + "SetUISize", &CTransform::SetUISize, + "GetUIPivot", [](CTransform& p_this) -> FVector2 { return p_this.GetUIPivot(); }, + "SetUIPivot", &CTransform::SetUIPivot, + "GetUIAnchorPreset", &CTransform::GetUIAnchorPreset, + "SetUIAnchorPreset", &CTransform::SetUIAnchorPreset ); p_luaState.new_enum("FrustumBehaviour", { @@ -203,6 +225,169 @@ 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", CTransform::EUIAnchorPreset::TOP_LEFT}, + {"TOP_CENTER", CTransform::EUIAnchorPreset::TOP_CENTER}, + {"TOP_RIGHT", CTransform::EUIAnchorPreset::TOP_RIGHT}, + {"MIDDLE_LEFT", CTransform::EUIAnchorPreset::MIDDLE_LEFT}, + {"CENTER", CTransform::EUIAnchorPreset::CENTER}, + {"MIDDLE_RIGHT", CTransform::EUIAnchorPreset::MIDDLE_RIGHT}, + {"BOTTOM_LEFT", CTransform::EUIAnchorPreset::BOTTOM_LEFT}, + {"BOTTOM_CENTER", CTransform::EUIAnchorPreset::BOTTOM_CENTER}, + {"BOTTOM_RIGHT", CTransform::EUIAnchorPreset::BOTTOM_RIGHT}, + {"HORIZONTAL_STRETCH_TOP", CTransform::EUIAnchorPreset::HORIZONTAL_STRETCH_TOP}, + {"HORIZONTAL_STRETCH_MIDDLE", CTransform::EUIAnchorPreset::HORIZONTAL_STRETCH_MIDDLE}, + {"HORIZONTAL_STRETCH_BOTTOM", CTransform::EUIAnchorPreset::HORIZONTAL_STRETCH_BOTTOM}, + {"VERTICAL_STRETCH_LEFT", CTransform::EUIAnchorPreset::VERTICAL_STRETCH_LEFT}, + {"VERTICAL_STRETCH_CENTER", CTransform::EUIAnchorPreset::VERTICAL_STRETCH_CENTER}, + {"VERTICAL_STRETCH_RIGHT", CTransform::EUIAnchorPreset::VERTICAL_STRETCH_RIGHT}, + {"STRETCH_BOTH", CTransform::EUIAnchorPreset::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, + "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, + "GetForceExpandWidth", &UI::CLayoutGroup::GetForceExpandWidth, + "SetForceExpandWidth", &UI::CLayoutGroup::SetForceExpandWidth, + "GetForceExpandHeight", &UI::CLayoutGroup::GetForceExpandHeight, + "SetForceExpandHeight", &UI::CLayoutGroup::SetForceExpandHeight + ); + + p_luaState.new_usertype("HorizontalLayout", + sol::base_classes, sol::bases(), + "GetSpacing", &UI::CHorizontalLayout::GetSpacing, + "SetSpacing", &UI::CHorizontalLayout::SetSpacing, + "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, + "GetForceExpandWidth", &UI::CHorizontalLayout::GetForceExpandWidth, + "SetForceExpandWidth", &UI::CHorizontalLayout::SetForceExpandWidth, + "GetForceExpandHeight", &UI::CHorizontalLayout::GetForceExpandHeight, + "SetForceExpandHeight", &UI::CHorizontalLayout::SetForceExpandHeight + ); + + p_luaState.new_usertype("VerticalLayout", + sol::base_classes, sol::bases(), + "GetSpacing", &UI::CVerticalLayout::GetSpacing, + "SetSpacing", &UI::CVerticalLayout::SetSpacing, + "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, + "GetForceExpandWidth", &UI::CVerticalLayout::GetForceExpandWidth, + "SetForceExpandWidth", &UI::CVerticalLayout::SetForceExpandWidth, + "GetForceExpandHeight", &UI::CVerticalLayout::GetForceExpandHeight, + "SetForceExpandHeight", &UI::CVerticalLayout::SetForceExpandHeight + ); + + 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, + "GetHorizontalAlignment", &UI::CText::GetHorizontalAlignment, + "SetHorizontalAlignment", &UI::CText::SetHorizontalAlignment, + "GetVerticalAlignment", &UI::CText::GetVerticalAlignment, + "SetVerticalAlignment", &UI::CText::SetVerticalAlignment + ); + p_luaState.new_usertype("Light", sol::base_classes, sol::bases(), "GetColor", &CPointLight::GetColor, diff --git a/Sources/OvEditor/include/OvEditor/Core/CameraController.h b/Sources/OvEditor/include/OvEditor/Core/CameraController.h index 8d9ec5778..e4167bab1 100644 --- a/Sources/OvEditor/include/OvEditor/Core/CameraController.h +++ b/Sources/OvEditor/include/OvEditor/Core/CameraController.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "OvEditor/Panels/Hierarchy.h" #include "OvEditor/Panels/AView.h" @@ -101,7 +102,7 @@ namespace OvEditor::Core void UnlockTargetActor(); private: - std::optional> GetTargetActor() const; + OvTools::Utils::OptRef GetTargetActor() const; void HandleCameraPanning(const OvMaths::FVector2& p_mouseOffset, bool p_firstMouse); void HandleCameraOrbit(OvCore::ECS::Actor& p_target, const OvMaths::FVector2& p_mouseOffset, bool p_firstMouse); void HandleCameraFPSMouse(const OvMaths::FVector2& p_mouseOffset, bool p_firstMouse); @@ -139,6 +140,6 @@ namespace OvEditor::Core float m_focusDistance = 15.0f; float m_focusLerpCoefficient = 8.0f; - std::optional> m_lockedActor = std::nullopt; + OvTools::Utils::OptRef m_lockedActor; }; -} \ No newline at end of file +} diff --git a/Sources/OvEditor/include/OvEditor/Core/Context.h b/Sources/OvEditor/include/OvEditor/Core/Context.h index a46fa17fb..758bc2c79 100644 --- a/Sources/OvEditor/include/OvEditor/Core/Context.h +++ b/Sources/OvEditor/include/OvEditor/Core/Context.h @@ -11,11 +11,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -96,7 +98,9 @@ namespace OvEditor::Core OvCore::ResourceManagement::TextureManager textureManager; OvCore::ResourceManagement::ShaderManager shaderManager; OvCore::ResourceManagement::MaterialManager materialManager; + OvCore::ResourceManagement::FontManager fontManager; OvCore::ResourceManagement::SoundManager soundManager; + OvCore::ResourceManagement::UIResourceRegistry uiResourceRegistry; 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..16443ab93 100644 --- a/Sources/OvEditor/include/OvEditor/Core/GizmoBehaviour.h +++ b/Sources/OvEditor/include/OvEditor/Core/GizmoBehaviour.h @@ -42,8 +42,18 @@ namespace OvEditor::Core * @param p_cameraPosition * @param p_operation * @param p_direction + * @param p_overrideWorldPosition + * @param p_uiUnitsScale */ - 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, + const float* p_uiUnitsScale = nullptr, + bool p_uiScreenSpace = false + ); /** * Stops the gizmo picking behaviour @@ -143,6 +153,10 @@ namespace OvEditor::Core EDirection m_direction; OvMaths::FTransform m_originalTransform; OvMaths::FVector3 m_initialOffset; + OvMaths::FVector2 m_originalUIPosition = OvMaths::FVector2::Zero; + float m_uiUnitsScale = 1.0f; + bool m_isUITranslation = false; + bool m_isUIScreenSpace = false; 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/Inspector.h b/Sources/OvEditor/include/OvEditor/Panels/Inspector.h index b49c63c28..729eee1ce 100644 --- a/Sources/OvEditor/include/OvEditor/Panels/Inspector.h +++ b/Sources/OvEditor/include/OvEditor/Panels/Inspector.h @@ -68,6 +68,7 @@ namespace OvEditor::Panels void _DrawAddSection(); void _DrawComponent(OvCore::ECS::Components::AComponent& p_component, int p_index, int p_total); void _DrawBehaviour(OvCore::ECS::Components::Behaviour& p_behaviour, int p_index, int p_total); + void _RefreshIfTargetHierarchyChanged(OvCore::ECS::Actor& p_changedActor); private: OvTools::Utils::OptRef m_targetActor = std::nullopt; @@ -78,5 +79,7 @@ namespace OvEditor::Panels uint64_t m_behaviourAddedListener = 0; uint64_t m_behaviourRemovedListener = 0; uint64_t m_destroyedListener = 0; + uint64_t m_attachedListener = 0; + uint64_t m_detachedListener = 0; }; -} \ 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/include/OvEditor/Rendering/DebugModelRenderFeature.h b/Sources/OvEditor/include/OvEditor/Rendering/DebugModelRenderFeature.h index ad4c112ac..99a149d39 100644 --- a/Sources/OvEditor/include/OvEditor/Rendering/DebugModelRenderFeature.h +++ b/Sources/OvEditor/include/OvEditor/Rendering/DebugModelRenderFeature.h @@ -6,6 +6,9 @@ #pragma once +#include + +#include #include namespace OvEditor::Rendering @@ -38,7 +41,9 @@ namespace OvEditor::Rendering OvRendering::Data::PipelineState p_pso, OvRendering::Resources::Model& p_model, OvRendering::Data::Material& p_material, - const OvMaths::FMatrix4& p_modelMatrix + const OvMaths::FMatrix4& p_modelMatrix, + std::optional p_viewMatrixOverride = std::nullopt, + std::optional p_projectionMatrixOverride = std::nullopt ); }; } diff --git a/Sources/OvEditor/include/OvEditor/Rendering/GizmoRenderFeature.h b/Sources/OvEditor/include/OvEditor/Rendering/GizmoRenderFeature.h index 36bd90f7a..38a1e0fec 100644 --- a/Sources/OvEditor/include/OvEditor/Rendering/GizmoRenderFeature.h +++ b/Sources/OvEditor/include/OvEditor/Rendering/GizmoRenderFeature.h @@ -6,6 +6,9 @@ #pragma once +#include + +#include #include #include @@ -50,11 +53,15 @@ namespace OvEditor::Rendering const OvMaths::FQuaternion& p_rotation, OvEditor::Core::EGizmoOperation p_operation, bool p_pickable, - std::optional p_highlightedDirection + std::optional p_highlightedDirection, + std::optional p_viewMatrixOverride = std::nullopt, + std::optional p_projectionMatrixOverride = std::nullopt, + std::optional p_scaleOverride = std::nullopt, + bool p_showZAxis = true ); private: OvCore::Resources::Material m_gizmoArrowMaterial; OvCore::Resources::Material m_gizmoBallMaterial; }; -} \ No newline at end of file +} diff --git a/Sources/OvEditor/include/OvEditor/Rendering/GridRenderPass.h b/Sources/OvEditor/include/OvEditor/Rendering/GridRenderPass.h index 66691c399..2df9b36c2 100644 --- a/Sources/OvEditor/include/OvEditor/Rendering/GridRenderPass.h +++ b/Sources/OvEditor/include/OvEditor/Rendering/GridRenderPass.h @@ -31,6 +31,7 @@ namespace OvEditor::Rendering { OvMaths::FVector3 gridColor; OvMaths::FVector3 viewPosition; + bool visible = true; }; /** @@ -45,4 +46,4 @@ namespace OvEditor::Rendering private: OvCore::Resources::Material m_gridMaterial; }; -} \ No newline at end of file +} diff --git a/Sources/OvEditor/include/OvEditor/Rendering/PickingRenderPass.h b/Sources/OvEditor/include/OvEditor/Rendering/PickingRenderPass.h index fb996835e..222ccf48c 100644 --- a/Sources/OvEditor/include/OvEditor/Rendering/PickingRenderPass.h +++ b/Sources/OvEditor/include/OvEditor/Rendering/PickingRenderPass.h @@ -6,6 +6,9 @@ #pragma once +#include + +#include #include #include #include @@ -62,7 +65,11 @@ namespace OvEditor::Rendering OvRendering::Data::PipelineState p_pso, const OvMaths::FVector3& p_position, const OvMaths::FQuaternion& p_rotation, - OvEditor::Core::EGizmoOperation p_operation + OvEditor::Core::EGizmoOperation p_operation, + std::optional p_viewMatrixOverride = std::nullopt, + std::optional p_projectionMatrixOverride = std::nullopt, + std::optional p_scaleOverride = std::nullopt, + bool p_showZAxis = true ); private: 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/CameraController.cpp b/Sources/OvEditor/src/OvEditor/Core/CameraController.cpp index 036c685be..084e44f8a 100644 --- a/Sources/OvEditor/src/OvEditor/Core/CameraController.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/CameraController.cpp @@ -4,6 +4,8 @@ * @licence: MIT */ +#include +#include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include #include #include @@ -27,68 +30,131 @@ OvEditor::Core::CameraController::CameraController( m_camera.SetFov(60.0f); } -float GetActorFocusDist(OvCore::ECS::Actor& p_actor) +namespace { - float distance = 4.0f; + struct ActorFocusTarget + { + OvMaths::FVector3 position = OvMaths::FVector3::Zero; + float distance = 4.0f; + }; - if (p_actor.IsActive()) + float GetActor3DFocusDist(OvCore::ECS::Actor& p_actor) { - if (auto pb = p_actor.GetComponent()) + float distance = 4.0f; + + if (p_actor.IsActive()) { - distance = std::max(distance, std::max - ( - std::max + if (auto pb = p_actor.GetComponent()) + { + distance = std::max(distance, std::max ( - pb->GetSize().x * p_actor.transform.GetWorldScale().x, - pb->GetSize().y * p_actor.transform.GetWorldScale().y - ), - pb->GetSize().z * p_actor.transform.GetWorldScale().z - ) * 1.5f); - } + std::max + ( + pb->GetSize().x * p_actor.transform.GetWorldScale().x, + pb->GetSize().y * p_actor.transform.GetWorldScale().y + ), + pb->GetSize().z * p_actor.transform.GetWorldScale().z + ) * 1.5f); + } - if (auto ps = p_actor.GetComponent()) - { - distance = std::max(distance, std::max - ( - std::max + if (auto ps = p_actor.GetComponent()) + { + distance = std::max(distance, std::max + ( + std::max + ( + ps->GetRadius() * p_actor.transform.GetWorldScale().x, + ps->GetRadius() * p_actor.transform.GetWorldScale().y + ), + ps->GetRadius() * p_actor.transform.GetWorldScale().z + ) * 1.5f); + } + + if (auto pc = p_actor.GetComponent()) + { + distance = std::max(distance, std::max ( - ps->GetRadius() * p_actor.transform.GetWorldScale().x, - ps->GetRadius() * p_actor.transform.GetWorldScale().y - ), - ps->GetRadius() * p_actor.transform.GetWorldScale().z - ) * 1.5f); + std::max + ( + pc->GetRadius() * p_actor.transform.GetWorldScale().x, + pc->GetHeight() * p_actor.transform.GetWorldScale().y + ), + pc->GetRadius() * p_actor.transform.GetWorldScale().z + ) * 1.5f); + } + + if (auto modelRenderer = p_actor.GetComponent()) + { + const bool hasCustomBoundingSphere = modelRenderer->GetFrustumBehaviour() == OvCore::ECS::Components::CModelRenderer::EFrustumBehaviour::CUSTOM_BOUNDS; + const bool hasModel = modelRenderer->GetModel(); + const auto boundingSphere = hasCustomBoundingSphere ? &modelRenderer->GetCustomBoundingSphere() : hasModel ? &modelRenderer->GetModel()->GetBoundingSphere() : nullptr; + const auto& actorScale = p_actor.transform.GetWorldScale(); + const auto scaleFactor = std::max(std::max(actorScale.x, actorScale.y), actorScale.z); + + distance = std::max(distance, boundingSphere ? (boundingSphere->radius + OvMaths::FVector3::Length(boundingSphere->position)) * scaleFactor * 2.0f : 10.0f); + } + + for (auto child : p_actor.GetChildren()) + distance = std::max(distance, GetActor3DFocusDist(*child)); } - if (auto pc = p_actor.GetComponent()) + return distance; + } + + bool TryGetUIFocusTarget( + OvCore::ECS::Actor& p_actor, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver, + ActorFocusTarget& p_outTarget + ) + { + OvCore::Rendering::UIRenderingUtils::ResolvedUIElement resolvedElement; + if (!p_uiFrameResolver.ResolveElement( + p_actor, + resolvedElement + )) { - distance = std::max(distance, std::max - ( - std::max - ( - pc->GetRadius() * p_actor.transform.GetWorldScale().x, - pc->GetHeight() * p_actor.transform.GetWorldScale().y - ), - pc->GetRadius() * p_actor.transform.GetWorldScale().z - ) * 1.5f); + return false; } - if (auto modelRenderer = p_actor.GetComponent()) + p_outTarget.position = OvCore::Rendering::UIRenderingUtils::TransformUIPoint( + resolvedElement.modelMatrix, + OvMaths::FVector2::Zero + ); + + const auto halfSize = resolvedElement.elementSize * 0.5f; + const std::array corners = { + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedElement.modelMatrix, { -halfSize.x, -halfSize.y }), + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedElement.modelMatrix, { halfSize.x, -halfSize.y }), + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedElement.modelMatrix, { halfSize.x, halfSize.y }), + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedElement.modelMatrix, { -halfSize.x, halfSize.y }) + }; + + float focusRadius = 0.0f; + for (const auto& corner : corners) { - const bool hasCustomBoundingSphere = modelRenderer->GetFrustumBehaviour() == OvCore::ECS::Components::CModelRenderer::EFrustumBehaviour::CUSTOM_BOUNDS; - const bool hasModel = modelRenderer->GetModel(); - const auto boundingSphere = hasCustomBoundingSphere ? &modelRenderer->GetCustomBoundingSphere() : hasModel ? &modelRenderer->GetModel()->GetBoundingSphere() : nullptr; - const auto& actorPosition = p_actor.transform.GetWorldPosition(); - const auto& actorScale = p_actor.transform.GetWorldScale(); - const auto scaleFactor = std::max(std::max(actorScale.x, actorScale.y), actorScale.z); - - distance = std::max(distance, boundingSphere ? (boundingSphere->radius + OvMaths::FVector3::Length(boundingSphere->position)) * scaleFactor * 2.0f : 10.0f); + focusRadius = std::max(focusRadius, OvMaths::FVector3::Length(corner - p_outTarget.position)); } - for (auto child : p_actor.GetChildren()) - distance = std::max(distance, GetActorFocusDist(*child)); + p_outTarget.distance = p_uiFrameResolver.IsScreenSpace() ? 4.0f : std::max(4.0f, focusRadius * 2.0f); + return true; } - return distance; + ActorFocusTarget GetActorFocusTarget( + OvCore::ECS::Actor& p_actor, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver + ) + { + ActorFocusTarget target; + if (TryGetUIFocusTarget(p_actor, p_uiFrameResolver, target)) + { + return target; + } + + return { + p_actor.transform.GetWorldPosition(), + GetActor3DFocusDist(p_actor) + }; + } } void OvEditor::Core::CameraController::HandleInputs(float p_deltaTime) @@ -106,19 +172,29 @@ void OvEditor::Core::CameraController::HandleInputs(float p_deltaTime) { if (auto target = GetTargetActor()) { - auto targetPos = target.value().get().transform.GetWorldPosition(); - - float dist = GetActorFocusDist(target.value().get()); + auto [winWidth, winHeight] = m_view.GetSafeSize(); + const auto renderSize = OvMaths::FVector2{ + winWidth > 0 ? static_cast(winWidth) : 1.0f, + winHeight > 0 ? static_cast(winHeight) : 1.0f + }; + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver uiFrameResolver{ + renderSize, + EDITOR_EXEC(IsSceneUIRenderingEnabled()) + }; + const auto focusTarget = GetActorFocusTarget( + target.value(), + uiFrameResolver + ); if (m_inputManager.IsKeyPressed(OvWindowing::Inputs::EKey::KEY_F)) { - MoveToTarget(target.value().get()); + MoveToTarget(target.value()); } - auto focusObjectFromAngle = [this, &targetPos, &dist]( const OvMaths::FVector3& offset) + auto focusObjectFromAngle = [this, focusTarget]( const OvMaths::FVector3& offset) { - auto camPos = targetPos + offset * dist; - auto direction = OvMaths::FVector3::Normalize(targetPos - camPos); + auto camPos = focusTarget.position + offset * focusTarget.distance; + auto direction = OvMaths::FVector3::Normalize(focusTarget.position - camPos); m_camera.SetRotation(OvMaths::FQuaternion::LookAt(direction, std::abs(direction.y) == 1.0f ? OvMaths::FVector3::Right : OvMaths::FVector3::Up)); m_cameraDestinations.push({ camPos, m_camera.GetRotation() }); }; @@ -193,7 +269,7 @@ void OvEditor::Core::CameraController::HandleInputs(float p_deltaTime) { if (auto target = GetTargetActor()) { - HandleCameraOrbit(target.value().get(), mouseOffset, wasFirstMouse); + HandleCameraOrbit(target.value(), mouseOffset, wasFirstMouse); } } else @@ -215,11 +291,25 @@ void OvEditor::Core::CameraController::HandleInputs(float p_deltaTime) void OvEditor::Core::CameraController::MoveToTarget(OvCore::ECS::Actor& p_target) { + auto [winWidth, winHeight] = m_view.GetSafeSize(); + const auto renderSize = OvMaths::FVector2{ + winWidth > 0 ? static_cast(winWidth) : 1.0f, + winHeight > 0 ? static_cast(winHeight) : 1.0f + }; + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver uiFrameResolver{ + renderSize, + EDITOR_EXEC(IsSceneUIRenderingEnabled()) + }; + const auto focusTarget = GetActorFocusTarget( + p_target, + uiFrameResolver + ); + m_cameraDestinations.push({ - p_target.transform.GetWorldPosition() - + focusTarget.position - m_camera.GetRotation() * OvMaths::FVector3::Forward * - GetActorFocusDist(p_target), + focusTarget.distance, m_camera.GetRotation() }); } @@ -266,15 +356,15 @@ bool OvEditor::Core::CameraController::IsOperating() const void OvEditor::Core::CameraController::LockTargetActor(OvCore::ECS::Actor& p_actor) { - m_lockedActor = p_actor; + m_lockedActor = &p_actor; } void OvEditor::Core::CameraController::UnlockTargetActor() { - m_lockedActor = std::nullopt; + m_lockedActor.reset(); } -std::optional> OvEditor::Core::CameraController::GetTargetActor() const +OvTools::Utils::OptRef OvEditor::Core::CameraController::GetTargetActor() const { if (m_lockedActor.has_value()) { @@ -282,7 +372,7 @@ std::optional> OvEditor::Core::Camera } else if (EDITOR_EXEC(IsAnyActorSelected())) { - return EDITOR_EXEC(GetSelectedActor()); + return OvTools::Utils::OptRef{ EDITOR_EXEC(GetSelectedActor()) }; } return std::nullopt; diff --git a/Sources/OvEditor/src/OvEditor/Core/Context.cpp b/Sources/OvEditor/src/OvEditor/Core/Context.cpp index 72f29b65d..370df0adf 100644 --- a/Sources/OvEditor/src/OvEditor/Core/Context.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/Context.cpp @@ -85,11 +85,17 @@ 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({ .shaderPath = ":Shaders/Standard.ovfx" }); + uiResourceRegistry.ProvideDefinition({ + .imageMaterialPath = ":Materials\\Image.ovmat", + .textMaterialPath = ":Materials\\Text.ovmat", + .defaultFontPath = ":Fonts\\Roboto-Regular.ttf" + }); /* Settings */ OvWindowing::Settings::DeviceSettings deviceSettings; @@ -168,7 +174,9 @@ 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(uiResourceRegistry); ServiceLocator::Provide(*inputManager); ServiceLocator::Provide(*window); ServiceLocator::Provide(sceneManager); @@ -185,6 +193,7 @@ OvEditor::Core::Context::~Context() textureManager.UnloadResources(); shaderManager.UnloadResources(); materialManager.UnloadResources(); + fontManager.UnloadResources(); soundManager.UnloadResources(); } diff --git a/Sources/OvEditor/src/OvEditor/Core/Editor.cpp b/Sources/OvEditor/src/OvEditor/Core/Editor.cpp index eb4b36bb4..f873f4eea 100644 --- a/Sources/OvEditor/src/OvEditor/Core/Editor.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/Editor.cpp @@ -76,6 +76,15 @@ void OvEditor::Core::Editor::SetupUI() } ); + OvCore::Helpers::GUIHelpers::SetPickerCloseProvider( + [this] { + if (m_itemPicker) + { + m_itemPicker->Close(); + } + } + ); + OvCore::Helpers::GUIHelpers::SetPickerSearchTextProvider( [this]() { return m_itemPicker->GetSearchText(); } ); diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 27d38924a..4e9aa92b9 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -26,6 +26,9 @@ #include #include #include +#include +#include +#include #include #include @@ -1030,6 +1033,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"); @@ -1512,15 +1534,17 @@ bool OvEditor::Core::EditorActions::ImportAsset(const std::string& p_initialDest std::string shaderPartFormats = "*.ovfxh;"; std::string soundFormats = "*.mp3;*.ogg;*.wav;"; std::string scriptFormats = "*.lua;"; + std::string fontFormats = "*.ttf;*.otf;"; OpenFileDialog selectAssetDialog("Select an asset to import"); - selectAssetDialog.AddFileType("Any supported format", modelFormats + textureFormats + shaderFormats + shaderPartFormats + soundFormats + scriptFormats); + selectAssetDialog.AddFileType("Any supported format", modelFormats + textureFormats + shaderFormats + shaderPartFormats + soundFormats + scriptFormats + fontFormats); selectAssetDialog.AddFileType("Model (.fbx, .obj)", modelFormats); selectAssetDialog.AddFileType("Texture (.png, .jpeg, .jpg, .tga, .hdr)", textureFormats); selectAssetDialog.AddFileType("Shader (.ovfx)", shaderFormats); selectAssetDialog.AddFileType("Shader Parts (.ovfxh)", shaderPartFormats); selectAssetDialog.AddFileType("Sound (.mp3, .ogg, .wav)", soundFormats); selectAssetDialog.AddFileType("Script (.lua)", scriptFormats); + selectAssetDialog.AddFileType("Font (.ttf, .otf)", fontFormats); selectAssetDialog.Show(); if (selectAssetDialog.HasSucceeded()) @@ -1597,15 +1621,17 @@ bool OvEditor::Core::EditorActions::ImportAssetAtLocation(const std::string& p_d std::string shaderPartFormats = "*.ovfxh;"; std::string soundFormats = "*.mp3;*.ogg;*.wav;"; std::string scriptFormats = "*.lua;"; + std::string fontFormats = "*.ttf;*.otf;"; OpenFileDialog selectAssetDialog("Select an asset to import"); - selectAssetDialog.AddFileType("Any supported format", modelFormats + textureFormats + shaderFormats + soundFormats + scriptFormats); + selectAssetDialog.AddFileType("Any supported format", modelFormats + textureFormats + shaderFormats + soundFormats + scriptFormats + fontFormats); selectAssetDialog.AddFileType("Model (.fbx, .obj)", modelFormats); selectAssetDialog.AddFileType("Texture (.png, .jpeg, .jpg, .tga, .hdr)", textureFormats); selectAssetDialog.AddFileType("Shader (.ovfx)", shaderFormats); selectAssetDialog.AddFileType("Shader Parts (.ovfxh)", shaderPartFormats); selectAssetDialog.AddFileType("Sound (.mp3, .ogg, .wav)", soundFormats); selectAssetDialog.AddFileType("Script (.lua)", scriptFormats); + selectAssetDialog.AddFileType("Font (.ttf, .otf)", fontFormats); selectAssetDialog.Show(); if (selectAssetDialog.HasSucceeded()) @@ -1860,6 +1886,12 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa const_cast(resource->path) = p_newName; } + if (OvCore::Global::ServiceLocator::Get().MoveResource(p_previousName, p_newName)) + { + OvRendering::Resources::Font* resource = OvCore::Global::ServiceLocator::Get()[p_newName]; + const_cast(resource->path) = p_newName; + } + if (OvTools::Utils::PathParser::GetFileType(p_previousName) == OvTools::Utils::PathParser::EFileType::MODEL) { MoveAllEmbeddedResourcesForRenamedModel(p_previousName, p_newName); @@ -1891,6 +1923,11 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa if (auto pval = std::get_if(&assetViewRes); pval && *pval) assetView.ClearResource(); + if (auto currentScene = m_context.sceneManager.GetCurrentScene()) + for (auto actor : currentScene->GetActors()) + if (auto image = actor->GetComponent(); image && image->GetTexture() == texture) + image->SetTexture(nullptr); + OvCore::Global::ServiceLocator::Get().UnloadResource(p_previousName); } @@ -1949,6 +1986,9 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa OvCore::Global::ServiceLocator::Get().UnloadResource(p_previousName); } + + if (OvCore::Global::ServiceLocator::Get().GetResource(p_previousName, false)) + OvCore::Global::ServiceLocator::Get().UnloadResource(p_previousName); } switch (OvTools::Utils::PathParser::GetFileType(p_previousName)) @@ -1994,9 +2034,20 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::MATERIAL); break; case OvTools::Utils::PathParser::EFileType::TEXTURE: + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::MATERIAL); break; case OvTools::Utils::PathParser::EFileType::SOUND: + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); + break; + case OvTools::Utils::PathParser::EFileType::FONT: + if (auto currentScene = m_context.sceneManager.GetCurrentScene()) + for (auto actor : currentScene->GetActors()) + if (auto text = actor->GetComponent(); text && text->GetFontPath() == p_previousName) + text->SetFontPath(p_newName); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); break; diff --git a/Sources/OvEditor/src/OvEditor/Core/GizmoBehaviour.cpp b/Sources/OvEditor/src/OvEditor/Core/GizmoBehaviour.cpp index 96a7bfe86..64ab30309 100644 --- a/Sources/OvEditor/src/OvEditor/Core/GizmoBehaviour.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/GizmoBehaviour.cpp @@ -5,6 +5,7 @@ */ #include +#include #include "OvEditor/Core/GizmoBehaviour.h" #include "OvEditor/Core/EditorActions.h" @@ -32,20 +33,47 @@ 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, + const float* p_uiUnitsScale, + bool p_uiScreenSpace +) { 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; + m_isUITranslation = p_target.transform.HasActiveUIData() && p_uiUnitsScale != nullptr; + m_isUIScreenSpace = m_isUITranslation && p_uiScreenSpace; + m_uiUnitsScale = p_uiUnitsScale ? std::max(std::abs(*p_uiUnitsScale), 0.0001f) : 1.0f; + + if (m_isUITranslation) + { + m_originalUIPosition = p_target.transform.GetUIPosition(); + } + else + { + m_originalUIPosition = OvMaths::FVector2::Zero; + } } void OvEditor::Core::GizmoBehaviour::StopPicking() { m_target = nullptr; + m_isUITranslation = false; + m_isUIScreenSpace = false; + m_uiUnitsScale = 1.0f; } OvMaths::FVector3 OvEditor::Core::GizmoBehaviour::GetFakeDirection() const @@ -122,6 +150,98 @@ 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 (m_isUITranslation) + { + if (m_direction == EDirection::X && !m_target->transform.IsHorizontalUIPositionEditable()) + { + return; + } + + if (m_direction == EDirection::Y && !m_target->transform.IsVerticalUIPositionEditable()) + { + return; + } + + if (m_isUIScreenSpace) + { + const auto mouseDelta = m_currentMouse - m_originMouse; + OvMaths::FVector2 axisDelta = OvMaths::FVector2::Zero; + + switch (m_direction) + { + case EDirection::X: + axisDelta.x = mouseDelta.x / m_uiUnitsScale; + break; + case EDirection::Y: + axisDelta.y = -mouseDelta.y / m_uiUnitsScale; + break; + case EDirection::Z: + break; + } + + if (IsSnappedBehaviourEnabled()) + { + axisDelta.x = SnapValue(axisDelta.x, OvEditor::Settings::EditorSettings::TranslationSnapUnit); + axisDelta.y = SnapValue(axisDelta.y, OvEditor::Settings::EditorSettings::TranslationSnapUnit); + } + + m_target->transform.SetUIPosition(m_originalUIPosition + axisDelta); + return; + } + + const auto ray = GetMouseRay(m_currentMouse, p_viewMatrix, p_projectionMatrix, p_viewSize); + const OvMaths::FVector3 direction = GetRealDirection(true); + const OvMaths::FVector3 planePoint = m_originalTransform.GetWorldPosition(); + const OvMaths::FVector3 planeTangent = OvMaths::FVector3::Cross(direction, planePoint - p_cameraPosition); + const OvMaths::FVector3 planeNormal = OvMaths::FVector3::Cross(direction, planeTangent); + + const float denom = OvMaths::FVector3::Dot(ray, planeNormal); + + if (std::abs(denom) <= 0.001f) + { + return; + } + + const float t = OvMaths::FVector3::Dot(planePoint - p_cameraPosition, planeNormal) / denom; + + if (t <= 0.001f) + { + return; + } + + const OvMaths::FVector3 point = p_cameraPosition + ray * t; + + if (m_firstPick) + { + m_initialOffset = m_originalTransform.GetWorldPosition() - point; + m_firstPick = false; + } + + const auto translationVector = point - planePoint + m_initialOffset; + auto translationPixels = OvMaths::FVector3::Dot(translationVector, direction) / m_uiUnitsScale; + + 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; + } + + m_target->transform.SetUIPosition(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..924f3a698 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp @@ -26,6 +26,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -66,6 +72,18 @@ 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 || + std::is_same_v + ) + { + p_actor.transform.EnableUIData(); + } + p_actor.AddComponent(); } @@ -75,6 +93,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 +131,12 @@ namespace CreateComponentInfo("Audio Listener"), CreateComponentInfo("Post Process Stack"), CreateComponentInfo("Reflection Probe"), + CreateComponentInfo("Canvas"), + CreateComponentInfo("Horizontal Layout"), + CreateComponentInfo("Image"), + CreateComponentInfo("Layout Group"), + CreateComponentInfo("Text"), + CreateComponentInfo("Vertical Layout"), }); } @@ -123,11 +155,21 @@ OvEditor::Panels::Inspector::Inspector( UnFocus(); } }; + + m_attachedListener = Actor::AttachEvent += [this](Actor& p_attached, Actor&) { + _RefreshIfTargetHierarchyChanged(p_attached); + }; + + m_detachedListener = Actor::DettachEvent += [this](Actor& p_detached) { + _RefreshIfTargetHierarchyChanged(p_detached); + }; } OvEditor::Panels::Inspector::~Inspector() { Actor::DestroyedEvent -= m_destroyedListener; + Actor::AttachEvent -= m_attachedListener; + Actor::DettachEvent -= m_detachedListener; UnFocus(); } @@ -142,8 +184,16 @@ void OvEditor::Panels::Inspector::FocusActor(Actor& p_target) m_componentAddedListener = m_targetActor->ComponentAddedEvent += [this] (auto&) { EDITOR_EXEC(DelayAction([this] { Refresh(); })); }; m_behaviourAddedListener = m_targetActor->BehaviourAddedEvent += [this](auto&) { EDITOR_EXEC(DelayAction([this] { Refresh(); })); }; - m_componentRemovedListener = m_targetActor->ComponentRemovedEvent += [this](auto&) { EDITOR_EXEC(DelayAction([this] { Refresh(); })); }; - m_behaviourRemovedListener = m_targetActor->BehaviourRemovedEvent += [this](auto&) { EDITOR_EXEC(DelayAction([this] { Refresh(); })); }; + m_componentRemovedListener = m_targetActor->ComponentRemovedEvent += [this](auto&) + { + OvCore::Helpers::GUIHelpers::ClosePicker(); + EDITOR_EXEC(DelayAction([this] { Refresh(); })); + }; + m_behaviourRemovedListener = m_targetActor->BehaviourRemovedEvent += [this](auto&) + { + OvCore::Helpers::GUIHelpers::ClosePicker(); + EDITOR_EXEC(DelayAction([this] { Refresh(); })); + }; _Populate(); @@ -162,6 +212,7 @@ void OvEditor::Panels::Inspector::UnFocus() m_targetActor->BehaviourAddedEvent -= m_behaviourAddedListener; m_targetActor->BehaviourRemovedEvent -= m_behaviourRemovedListener; + OvCore::Helpers::GUIHelpers::ClosePicker(); m_content->RemoveAllWidgets(); EDITOR_EVENT(ActorUnselectedEvent).Invoke(m_targetActor.value()); @@ -366,7 +417,8 @@ void OvEditor::Panels::Inspector::_DrawComponent(AComponent& p_component, int p_ auto& header = m_content->CreateWidget(p_component.GetName()); const bool isTransform = dynamic_cast(&p_component) != nullptr; header.closable = !isTransform; - header.CloseEvent += [this, &header, &p_component] { + header.CloseEvent += [&p_component] { + OvCore::Helpers::GUIHelpers::ClosePicker(); p_component.owner.RemoveComponent(p_component); }; @@ -403,6 +455,7 @@ void OvEditor::Panels::Inspector::_DrawBehaviour(Behaviour& p_behaviour, int p_i auto& header = m_content->CreateWidget(std::filesystem::path(p_behaviour.name).replace_extension().string()); header.closable = true; header.CloseEvent += [&p_behaviour] { + OvCore::Helpers::GUIHelpers::ClosePicker(); p_behaviour.owner.RemoveBehaviour(p_behaviour); }; @@ -445,3 +498,19 @@ void OvEditor::Panels::Inspector::Refresh() _Populate(); } } + +void OvEditor::Panels::Inspector::_RefreshIfTargetHierarchyChanged(Actor& p_changedActor) +{ + if (!m_targetActor) + { + return; + } + + auto& targetActor = m_targetActor.value(); + if (&targetActor != &p_changedActor && !targetActor.IsDescendantOf(&p_changedActor)) + { + return; + } + + EDITOR_EXEC(DelayAction([this] { Refresh(); })); +} diff --git a/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp b/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp index 3cb964f59..f110a3000 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp @@ -4,9 +4,13 @@ * @licence: MIT */ +#include + #include +#include #include +#include #include #include #include @@ -17,6 +21,12 @@ namespace { + struct UIGizmoContext + { + OvMaths::FVector3 origin = OvMaths::FVector3::Zero; + float unitsScale = 1.0f; + }; + OvTools::Utils::OptRef GetActorFromPickingResult( OvEditor::Rendering::PickingRenderPass::PickingResult p_result ) @@ -31,6 +41,26 @@ namespace return std::nullopt; } + + std::optional ResolveUIGizmoContext( + OvCore::ECS::Actor& p_actor, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver + ) + { + OvCore::Rendering::UIRenderingUtils::ResolvedUIElement resolvedElement; + if (!p_uiFrameResolver.ResolveElement( + p_actor, + resolvedElement + )) + { + return std::nullopt; + } + + return UIGizmoContext{ + OvCore::Rendering::UIRenderingUtils::TransformUIElementPivot(resolvedElement), + resolvedElement.unitsScale + }; + } } OvEditor::Panels::SceneView::SceneView @@ -104,6 +134,12 @@ void OvEditor::Panels::SceneView::InitFrame() { AViewControllable::InitFrame(); + m_renderer->SetDescriptor({ + m_gridColor, + m_camera.GetPosition(), + !EDITOR_EXEC(IsSceneUIRenderingEnabled()) + }); + OvTools::Utils::OptRef selectedActor; if (EDITOR_EXEC(IsAnyActorSelected())) @@ -148,6 +184,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 +260,32 @@ 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 OvCore::Rendering::UIRenderingUtils::UIFrameResolver uiFrameResolver{ + renderSize, + EDITOR_EXEC(IsSceneUIRenderingEnabled()) + }; + const auto uiGizmoContext = ResolveUIGizmoContext( + selectedActor, + uiFrameResolver + ); + const auto* uiOrigin = uiGizmoContext ? &uiGizmoContext->origin : nullptr; + const auto* uiUnitsScale = uiGizmoContext ? &uiGizmoContext->unitsScale : nullptr; + m_gizmoOperations.StartPicking( - EDITOR_EXEC(GetSelectedActor()), + selectedActor, m_camera.GetPosition(), m_currentOperation, - m_highlightedGizmoDirection.value()); + m_highlightedGizmoDirection.value(), + uiOrigin, + uiUnitsScale, + uiFrameResolver.IsScreenSpace() + ); } 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/DebugModelRenderFeature.cpp b/Sources/OvEditor/src/OvEditor/Rendering/DebugModelRenderFeature.cpp index 93bd74d15..a0db34546 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/DebugModelRenderFeature.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/DebugModelRenderFeature.cpp @@ -16,13 +16,22 @@ OvEditor::Rendering::DebugModelRenderFeature::DebugModelRenderFeature( { } -void OvEditor::Rendering::DebugModelRenderFeature::DrawModelWithSingleMaterial(OvRendering::Data::PipelineState p_pso, OvRendering::Resources::Model& p_model, OvRendering::Data::Material& p_material, const OvMaths::FMatrix4& p_modelMatrix) +void OvEditor::Rendering::DebugModelRenderFeature::DrawModelWithSingleMaterial( + OvRendering::Data::PipelineState p_pso, + OvRendering::Resources::Model& p_model, + OvRendering::Data::Material& p_material, + const OvMaths::FMatrix4& p_modelMatrix, + std::optional p_viewMatrixOverride, + std::optional p_projectionMatrixOverride +) { auto stateMask = p_material.GenerateStateMask(); auto engineDrawableDescriptor = OvCore::Rendering::EngineDrawableDescriptor{ p_modelMatrix, - OvMaths::FMatrix4::Identity + OvMaths::FMatrix4::Identity, + p_viewMatrixOverride, + p_projectionMatrixOverride }; for (auto mesh : p_model.GetMeshes()) diff --git a/Sources/OvEditor/src/OvEditor/Rendering/DebugSceneRenderer.cpp b/Sources/OvEditor/src/OvEditor/Rendering/DebugSceneRenderer.cpp index f05d274f4..eb8aec3a5 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/DebugSceneRenderer.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/DebugSceneRenderer.cpp @@ -4,7 +4,11 @@ * @licence: MIT */ +#include +#include + #include +#include #include #include #include @@ -12,8 +16,13 @@ #include #include #include +#include +#include +#include +#include #include #include +#include #include @@ -44,12 +53,17 @@ 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; + constexpr float kUIScreenSpaceGizmoScale = 80.0f; + constexpr float kUIScreenSpaceGizmoDepth = 1000.0f; OvMaths::FMatrix4 CalculateUnscaledModelMatrix(OvCore::ECS::Actor& p_actor) { @@ -99,16 +113,43 @@ namespace { auto lightBuffer = std::make_unique(); - const auto lightMatrices = std::to_array({ + const std::array lightMatrices = { CreateDebugDirectionalLight(), CreateDebugAmbientLight() - }); + }; lightBuffer->Allocate(sizeof(lightMatrices), OvRendering::Settings::EAccessSpecifier::STATIC_READ); lightBuffer->Upload(lightMatrices.data()); return lightBuffer; } + + bool TryGetUIActorGizmoTransform( + bool p_includeUI, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver, + OvCore::ECS::Actor& p_actor, + OvMaths::FVector3& p_position, + OvMaths::FQuaternion& p_rotation + ) + { + if (!p_includeUI) + { + return false; + } + + OvCore::Rendering::UIRenderingUtils::ResolvedUIElement resolvedElement; + if (!p_uiFrameResolver.ResolveElement( + p_actor, + resolvedElement + )) + { + return false; + } + + p_position = OvCore::Rendering::UIRenderingUtils::TransformUIElementPivot(resolvedElement); + p_rotation = p_actor.transform.GetWorldRotation(); + return true; + } } class DebugCamerasRenderPass : public OvRendering::Core::ARenderPass @@ -332,20 +373,56 @@ class DebugActorRenderPass : public OvRendering::Core::ARenderPass const bool isActorHovered = debugSceneDescriptor.highlightedActor && debugSceneDescriptor.highlightedActor->GetID() == selectedActor.GetID(); DrawActorDebugElements(selectedActor); + if (!selectedActor.GetComponent()) + { + if (auto* canvasOwner = OvCore::Rendering::UIRenderingUtils::FindCanvasOwner(selectedActor)) + { + DrawCanvasBounds(*canvasOwner); + } + } + auto gizmoPosition = selectedActor.transform.GetWorldPosition(); + auto gizmoRotation = selectedActor.transform.GetWorldRotation(); + const auto& sceneDescriptor = m_renderer.GetDescriptor(); + const auto& uiFrameResolver = m_renderer.GetDescriptor(); + const bool hasUIGizmoTransform = TryGetUIActorGizmoTransform( + sceneDescriptor.includeUI, + uiFrameResolver, + selectedActor, + gizmoPosition, + gizmoRotation + ); + std::optional gizmoViewMatrixOverride; + std::optional gizmoProjectionMatrixOverride; + std::optional gizmoScaleOverride; + bool showGizmoZAxis = true; + if (hasUIGizmoTransform && uiFrameResolver.IsScreenSpace()) + { + gizmoViewMatrixOverride = OvMaths::FMatrix4::Identity; + gizmoProjectionMatrixOverride = uiFrameResolver.CreateProjectionMatrix( + -kUIScreenSpaceGizmoDepth, + kUIScreenSpaceGizmoDepth + ); + gizmoScaleOverride = kUIScreenSpaceGizmoScale; + showGizmoZAxis = false; + } 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 + debugSceneDescriptor.highlightedGizmoDirection, + gizmoViewMatrixOverride, + gizmoProjectionMatrixOverride, + gizmoScaleOverride, + showGizmoZAxis ); } @@ -365,6 +442,11 @@ class DebugActorRenderPass : public OvRendering::Core::ARenderPass { if (p_actor.IsActive()) { + if (auto layout = p_actor.GetComponent()) + { + DrawUIBounds(p_actor, layout->GetComputedSize()); + } + /* Render static mesh outline and bounding spheres */ if (OvEditor::Settings::EditorSettings::ShowGeometryBounds) { @@ -416,6 +498,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 (p_actor.GetComponent()) + { + DrawCanvasBounds(p_actor); + } + for (auto& child : p_actor.GetChildren()) { DrawActorDebugElements(*child); @@ -423,6 +520,104 @@ 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& frameDescriptor = m_renderer.GetFrameDescriptor(); + const auto& uiFrameResolver = m_renderer.GetDescriptor(); + + OvCore::Rendering::UIRenderingUtils::ResolvedUIElement resolvedElement; + if (!uiFrameResolver.ResolveElement( + p_actor, + p_size, + resolvedElement + )) + { + return; + } + + const auto halfSize = p_size * 0.5f; + const std::array corners = { + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedElement.modelMatrix, { -halfSize.x, -halfSize.y }), + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedElement.modelMatrix, { halfSize.x, -halfSize.y }), + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedElement.modelMatrix, { halfSize.x, halfSize.y }), + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedElement.modelMatrix, { -halfSize.x, halfSize.y }) + }; + + auto pso = m_renderer.CreatePipelineState(); + if (uiFrameResolver.IsScreenSpace()) + { + m_debugShapeFeature.SetViewProjection(uiFrameResolver.CreateProjectionMatrix()); + } + + 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); + + if (uiFrameResolver.IsScreenSpace()) + { + const auto& camera = frameDescriptor.camera; + m_debugShapeFeature.SetViewProjection(camera->GetProjectionMatrix() * camera->GetViewMatrix()); + } + } + + void DrawCanvasBounds(OvCore::ECS::Actor& p_actor) + { + const auto& sceneDescriptor = m_renderer.GetDescriptor(); + if (!sceneDescriptor.includeUI) + { + return; + } + + const auto& frameDescriptor = m_renderer.GetFrameDescriptor(); + const auto& uiFrameResolver = m_renderer.GetDescriptor(); + + OvCore::Rendering::UIRenderingUtils::ResolvedUICanvas resolvedCanvas; + if (!uiFrameResolver.ResolveCanvas( + p_actor, + resolvedCanvas + )) + { + return; + } + + const auto halfSize = resolvedCanvas.size * 0.5f; + const std::array corners = { + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedCanvas.modelMatrix, { -halfSize.x, -halfSize.y }), + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedCanvas.modelMatrix, { halfSize.x, -halfSize.y }), + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedCanvas.modelMatrix, { halfSize.x, halfSize.y }), + OvCore::Rendering::UIRenderingUtils::TransformUIPoint(resolvedCanvas.modelMatrix, { -halfSize.x, halfSize.y }) + }; + + auto pso = m_renderer.CreatePipelineState(); + if (uiFrameResolver.IsScreenSpace()) + { + m_debugShapeFeature.SetViewProjection(uiFrameResolver.CreateProjectionMatrix()); + } + + 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); + + if (uiFrameResolver.IsScreenSpace()) + { + const auto& camera = frameDescriptor.camera; + m_debugShapeFeature.SetViewProjection(camera->GetProjectionMatrix() * camera->GetViewMatrix()); + } + } + void DrawFrustumLines( const OvMaths::FVector3& pos, const OvMaths::FVector3& forward, diff --git a/Sources/OvEditor/src/OvEditor/Rendering/GizmoRenderFeature.cpp b/Sources/OvEditor/src/OvEditor/Rendering/GizmoRenderFeature.cpp index c3bb577d8..404954802 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/GizmoRenderFeature.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/GizmoRenderFeature.cpp @@ -26,6 +26,13 @@ #include "OvEditor/Core/EditorActions.h" #include "OvEditor/Rendering/GizmoRenderFeature.h" +namespace +{ + constexpr float kDistanceBasedGizmoScale = -1.0f; + constexpr const char* kGizmoScaleUniform = "u_GizmoScale"; + constexpr const char* kShowZAxisUniform = "u_ShowZAxis"; +} + OvEditor::Rendering::GizmoRenderFeature::GizmoRenderFeature( OvRendering::Core::CompositeRenderer& p_renderer, OvRendering::Features::EFeatureExecutionPolicy p_executionPolicy @@ -37,11 +44,15 @@ OvEditor::Rendering::GizmoRenderFeature::GizmoRenderFeature( m_gizmoArrowMaterial.SetGPUInstances(3); m_gizmoArrowMaterial.SetProperty("u_IsBall", false); m_gizmoArrowMaterial.SetProperty("u_IsPickable", false); + m_gizmoArrowMaterial.TrySetProperty(kGizmoScaleUniform, kDistanceBasedGizmoScale); + m_gizmoArrowMaterial.TrySetProperty(kShowZAxisUniform, true); /* Gizmo Ball Material */ m_gizmoBallMaterial.SetShader(EDITOR_CONTEXT(editorResources)->GetShader("Gizmo")); m_gizmoBallMaterial.SetProperty("u_IsBall", true); m_gizmoBallMaterial.SetProperty("u_IsPickable", false); + m_gizmoBallMaterial.TrySetProperty(kGizmoScaleUniform, kDistanceBasedGizmoScale); + m_gizmoBallMaterial.TrySetProperty(kShowZAxisUniform, true); } std::string GetArrowModelName(OvEditor::Core::EGizmoOperation p_operation) @@ -67,10 +78,19 @@ void OvEditor::Rendering::GizmoRenderFeature::DrawGizmo( const OvMaths::FQuaternion& p_rotation, OvEditor::Core::EGizmoOperation p_operation, bool p_pickable, - std::optional p_highlightedDirection + std::optional p_highlightedDirection, + std::optional p_viewMatrixOverride, + std::optional p_projectionMatrixOverride, + std::optional p_scaleOverride, + bool p_showZAxis ) { auto pso = m_renderer.CreatePipelineState(); + const float gizmoScale = p_scaleOverride.value_or(kDistanceBasedGizmoScale); + m_gizmoBallMaterial.TrySetProperty(kGizmoScaleUniform, gizmoScale); + m_gizmoArrowMaterial.TrySetProperty(kGizmoScaleUniform, gizmoScale); + m_gizmoBallMaterial.TrySetProperty(kShowZAxisUniform, p_showZAxis); + m_gizmoArrowMaterial.TrySetProperty(kShowZAxisUniform, p_showZAxis); auto modelMatrix = OvMaths::FMatrix4::Translation(p_position) * @@ -85,7 +105,9 @@ void OvEditor::Rendering::GizmoRenderFeature::DrawGizmo( pso, *sphereModel, m_gizmoBallMaterial, - sphereModelMatrix + sphereModelMatrix, + p_viewMatrixOverride, + p_projectionMatrixOverride ); } @@ -101,7 +123,9 @@ void OvEditor::Rendering::GizmoRenderFeature::DrawGizmo( pso, *arrowModel, m_gizmoArrowMaterial, - modelMatrix + modelMatrix, + p_viewMatrixOverride, + p_projectionMatrixOverride ); } } diff --git a/Sources/OvEditor/src/OvEditor/Rendering/GridRenderPass.cpp b/Sources/OvEditor/src/OvEditor/Rendering/GridRenderPass.cpp index 3eca1dacf..ee88ec57c 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/GridRenderPass.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/GridRenderPass.cpp @@ -38,6 +38,11 @@ void OvEditor::Rendering::GridRenderPass::Draw(OvRendering::Data::PipelineState auto& gridDescriptor = m_renderer.GetDescriptor(); auto& debugShapeRenderer = m_renderer.GetFeature(); + if (!gridDescriptor.visible) + { + return; + } + auto pso = m_renderer.CreatePipelineState(); constexpr float gridSize = 5000.0f; diff --git a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp index 688c03e91..f5ec2c51e 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,11 @@ namespace { const std::string kPickingPassName = "PICKING_PASS"; const std::string kSkinningFeatureName = std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }; + constexpr float kDistanceBasedGizmoScale = -1.0f; + constexpr float kUIScreenSpaceGizmoScale = 80.0f; + constexpr float kUIScreenSpaceGizmoDepth = 1000.0f; + constexpr const char* kGizmoScaleUniform = "u_GizmoScale"; + constexpr const char* kShowZAxisUniform = "u_ShowZAxis"; void PreparePickingMaterial( const OvCore::ECS::Actor& p_actor, @@ -44,6 +50,33 @@ namespace p_material.SetProperty(p_uniformName, color, true); } } + + bool TryGetUIActorGizmoTransform( + const bool p_includeUI, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver, + OvCore::ECS::Actor& p_actor, + OvMaths::FVector3& p_position, + OvMaths::FQuaternion& p_rotation + ) + { + if (!p_includeUI) + { + return false; + } + + OvCore::Rendering::UIRenderingUtils::ResolvedUIElement resolvedElement; + if (!p_uiFrameResolver.ResolveElement( + p_actor, + resolvedElement + )) + { + return false; + } + + p_position = OvCore::Rendering::UIRenderingUtils::TransformUIElementPivot(resolvedElement); + p_rotation = p_actor.transform.GetWorldRotation(); + return true; + } } OvEditor::Rendering::PickingRenderPass::PickingRenderPass(OvRendering::Core::CompositeRenderer& p_renderer) : @@ -63,6 +96,8 @@ OvEditor::Rendering::PickingRenderPass::PickingRenderPass(OvRendering::Core::Com m_gizmoPickingMaterial.SetGPUInstances(3); m_gizmoPickingMaterial.SetProperty("u_IsBall", false); m_gizmoPickingMaterial.SetProperty("u_IsPickable", true); + m_gizmoPickingMaterial.TrySetProperty(kGizmoScaleUniform, kDistanceBasedGizmoScale); + m_gizmoPickingMaterial.TrySetProperty(kShowZAxisUniform, true); m_gizmoPickingMaterial.SetDepthTest(true); m_reflectionProbeMaterial.SetShader(EDITOR_CONTEXT(editorResources)->GetShader("PickingFallback")); @@ -121,6 +156,7 @@ void OvEditor::Rendering::PickingRenderPass::Draw(OvRendering::Data::PipelineSta auto& debugSceneDescriptor = m_renderer.GetDescriptor(); auto& frameDescriptor = m_renderer.GetFrameDescriptor(); auto& scene = sceneDescriptor.scene; + const auto& uiFrameResolver = m_renderer.GetDescriptor(); m_actorPickingFramebuffer.Resize(frameDescriptor.renderWidth, frameDescriptor.renderHeight); @@ -141,12 +177,39 @@ 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(); + const bool hasUIGizmoTransform = TryGetUIActorGizmoTransform( + sceneDescriptor.includeUI, + uiFrameResolver, + selectedActor, + gizmoPosition, + gizmoRotation + ); + std::optional gizmoViewMatrixOverride; + std::optional gizmoProjectionMatrixOverride; + std::optional gizmoScaleOverride; + bool showGizmoZAxis = true; + if (hasUIGizmoTransform && uiFrameResolver.IsScreenSpace()) + { + gizmoViewMatrixOverride = OvMaths::FMatrix4::Identity; + gizmoProjectionMatrixOverride = uiFrameResolver.CreateProjectionMatrix( + -kUIScreenSpaceGizmoDepth, + kUIScreenSpaceGizmoDepth + ); + gizmoScaleOverride = kUIScreenSpaceGizmoScale; + showGizmoZAxis = false; + } DrawPickableGizmo( pso, - selectedActor.transform.GetWorldPosition(), - selectedActor.transform.GetWorldRotation(), - debugSceneDescriptor.gizmoOperation + gizmoPosition, + gizmoRotation, + debugSceneDescriptor.gizmoOperation, + gizmoViewMatrixOverride, + gizmoProjectionMatrixOverride, + gizmoScaleOverride, + showGizmoZAxis ); } @@ -300,9 +363,16 @@ void OvEditor::Rendering::PickingRenderPass::DrawPickableGizmo( OvRendering::Data::PipelineState p_pso, const OvMaths::FVector3& p_position, const OvMaths::FQuaternion& p_rotation, - OvEditor::Core::EGizmoOperation p_operation + OvEditor::Core::EGizmoOperation p_operation, + std::optional p_viewMatrixOverride, + std::optional p_projectionMatrixOverride, + std::optional p_scaleOverride, + bool p_showZAxis ) { + m_gizmoPickingMaterial.TrySetProperty(kGizmoScaleUniform, p_scaleOverride.value_or(kDistanceBasedGizmoScale)); + m_gizmoPickingMaterial.TrySetProperty(kShowZAxisUniform, p_showZAxis); + auto modelMatrix = OvMaths::FMatrix4::Translation(p_position) * OvMaths::FQuaternion::ToMatrix4(OvMaths::FQuaternion::Normalize(p_rotation)); @@ -310,5 +380,12 @@ void OvEditor::Rendering::PickingRenderPass::DrawPickableGizmo( auto arrowModel = EDITOR_CONTEXT(editorResources)->GetModel("Arrow_Picking"); m_renderer.GetFeature() - .DrawModelWithSingleMaterial(p_pso, *arrowModel, m_gizmoPickingMaterial, modelMatrix); + .DrawModelWithSingleMaterial( + p_pso, + *arrowModel, + m_gizmoPickingMaterial, + modelMatrix, + p_viewMatrixOverride, + p_projectionMatrixOverride + ); } diff --git a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp index 6e8ae56e5..37d7a1efb 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp @@ -19,12 +19,18 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include #include +#include #include #include @@ -131,7 +137,17 @@ namespace { return [p_parent, p_onItemClicked]() { - EDITOR_EXEC(CreateMonoComponentActor(true, ResolveAliveParent(p_parent))); + auto& instance = EDITOR_EXEC(CreateMonoComponentActor(true, ResolveAliveParent(p_parent))); + if constexpr ( + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v + ) + { + instance.transform.EnableUIData(); + } if (p_onItemClicked.has_value()) { @@ -192,6 +208,65 @@ 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.transform.EnableUIData(); + 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.transform.EnableUIData(); + 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))); + instance.transform.EnableUIData(); + 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 +322,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 +345,11 @@ 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); 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..99c91bc6d 100644 --- a/Sources/OvGame/include/OvGame/Core/Context.h +++ b/Sources/OvGame/include/OvGame/Core/Context.h @@ -17,11 +17,13 @@ #include +#include #include #include #include #include #include +#include #include #include @@ -67,8 +69,10 @@ namespace OvGame::Core OvCore::ResourceManagement::TextureManager textureManager; OvCore::ResourceManagement::ShaderManager shaderManager; OvCore::ResourceManagement::MaterialManager materialManager; + OvCore::ResourceManagement::FontManager fontManager; OvCore::ResourceManagement::SoundManager soundManager; + OvCore::ResourceManagement::UIResourceRegistry uiResourceRegistry; 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..42f94da93 100644 --- a/Sources/OvGame/src/OvGame/Core/Context.cpp +++ b/Sources/OvGame/src/OvGame/Core/Context.cpp @@ -57,11 +57,17 @@ 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({ .shaderPath = ":Shaders/Standard.ovfx" }); + uiResourceRegistry.ProvideDefinition({ + .imageMaterialPath = ":Materials\\Image.ovmat", + .textMaterialPath = ":Materials\\Text.ovmat", + .defaultFontPath = ":Fonts\\Roboto-Regular.ttf" + }); /* Settings */ OvWindowing::Settings::DeviceSettings deviceSettings; @@ -161,7 +167,9 @@ OvGame::Core::Context::Context() : ServiceLocator::Provide(textureManager); ServiceLocator::Provide(shaderManager); ServiceLocator::Provide(materialManager); + ServiceLocator::Provide(fontManager); ServiceLocator::Provide(soundManager); + ServiceLocator::Provide(uiResourceRegistry); ServiceLocator::Provide(*inputManager); ServiceLocator::Provide(*window); ServiceLocator::Provide(sceneManager); @@ -184,5 +192,6 @@ OvGame::Core::Context::~Context() textureManager.UnloadResources(); shaderManager.UnloadResources(); materialManager.UnloadResources(); + fontManager.UnloadResources(); soundManager.UnloadResources(); } diff --git a/Sources/OvRendering/include/OvRendering/Features/DebugShapeRenderFeature.h b/Sources/OvRendering/include/OvRendering/Features/DebugShapeRenderFeature.h index 9f02bb1ad..92df5cd55 100644 --- a/Sources/OvRendering/include/OvRendering/Features/DebugShapeRenderFeature.h +++ b/Sources/OvRendering/include/OvRendering/Features/DebugShapeRenderFeature.h @@ -6,6 +6,8 @@ #pragma once +#include + #include "OvRendering/Features/ARenderFeature.h" namespace OvRendering::Features @@ -48,6 +50,12 @@ namespace OvRendering::Features bool p_depthTest = true ); + /** + * Overrides the view-projection matrix used by subsequent debug lines + * @param p_viewProjection + */ + void SetViewProjection(const OvMaths::FMatrix4& p_viewProjection); + /** * Draw a box in world space * @param p_pso @@ -116,4 +124,4 @@ namespace OvRendering::Features std::unique_ptr m_lineMesh; std::unique_ptr m_lineMaterial; }; -} \ No newline at end of file +} diff --git a/Sources/OvRendering/include/OvRendering/Resources/Font.h b/Sources/OvRendering/include/OvRendering/Resources/Font.h new file mode 100644 index 000000000..5c7d6ca2d --- /dev/null +++ b/Sources/OvRendering/include/OvRendering/Resources/Font.h @@ -0,0 +1,184 @@ +/** +* @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 resource revision, incremented when atlas variants are rebuilt + */ + uint64_t GetRevision() 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; + uint64_t m_revision = 0; + 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/Features/DebugShapeRenderFeature.cpp b/Sources/OvRendering/src/OvRendering/Features/DebugShapeRenderFeature.cpp index 03db3a2c6..31228129b 100644 --- a/Sources/OvRendering/src/OvRendering/Features/DebugShapeRenderFeature.cpp +++ b/Sources/OvRendering/src/OvRendering/Features/DebugShapeRenderFeature.cpp @@ -85,7 +85,12 @@ void OvRendering::Features::DebugShapeRenderFeature::OnBeginFrame(const Data::Fr p_frameDescriptor.camera->GetProjectionMatrix() * p_frameDescriptor.camera->GetViewMatrix(); - m_lineMaterial->SetProperty("viewProjection", viewProjection); + SetViewProjection(viewProjection); +} + +void OvRendering::Features::DebugShapeRenderFeature::SetViewProjection(const OvMaths::FMatrix4& p_viewProjection) +{ + m_lineMaterial->SetProperty("viewProjection", p_viewProjection); } void OvRendering::Features::DebugShapeRenderFeature::DrawLine( diff --git a/Sources/OvRendering/src/OvRendering/Resources/Font.cpp b/Sources/OvRendering/src/OvRendering/Resources/Font.cpp new file mode 100644 index 000000000..1ceaabf24 --- /dev/null +++ b/Sources/OvRendering/src/OvRendering/Resources/Font.cpp @@ -0,0 +1,630 @@ +/** +* @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(); + ++m_revision; + + 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; +} + +uint64_t OvRendering::Resources::Font::GetRevision() const +{ + return m_revision; +} + +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"