diff --git a/Resources/Editor/Shaders/Gizmo.ovfx b/Resources/Editor/Shaders/Gizmo.ovfx index 62d19b6a9..1641d559e 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 int u_VisibleAxes; uniform int u_HighlightedAxis; +uniform float u_GizmoScale; mat4 rotationMatrix(vec3 axis, float angle) { @@ -41,6 +43,17 @@ mat4 rotationMatrix(vec3 axis, float angle) void main() { + /* Instances are ordered Z, X, Y, while axis bits are ordered X, Y, Z */ + int axisBit = gl_InstanceID == 1 ? 1 : (gl_InstanceID == 2 ? 2 : 4); + + /* The ball is drawn with a single instance and isn't tied to an axis */ + if (!u_IsBall && (u_VisibleAxes & axisBit) == 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 +62,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 +124,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..578d61157 --- /dev/null +++ b/Resources/Engine/Lua/Components/Canvas.lua @@ -0,0 +1,63 @@ +---@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 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..23e0c5093 --- /dev/null +++ b/Resources/Engine/Lua/Components/Image.lua @@ -0,0 +1,41 @@ +---@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 + +--- Returns whether the image keeps its intrinsic aspect ratio when resized +---@return boolean +function Image:GetPreserveAspect() end + +--- Defines whether the image keeps its intrinsic aspect ratio when resized +---@param preserveAspect boolean +function Image:SetPreserveAspect(preserveAspect) end diff --git a/Resources/Engine/Lua/Components/LayoutGroup.lua b/Resources/Engine/Lua/Components/LayoutGroup.lua new file mode 100644 index 000000000..fd44a97ce --- /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 child layout slots expand to use all available width +---@return boolean +function LayoutGroup:GetForceExpandWidth() end + +--- Defines whether child layout slots expand to use all available width +---@param value boolean +function LayoutGroup:SetForceExpandWidth(value) end + +--- Returns whether child layout slots expand to use all available height +---@return boolean +function LayoutGroup:GetForceExpandHeight() end + +--- Defines whether child layout slots expand to use all available 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..90cf7ba95 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,109 @@ namespace OvCore::ECS::Components */ OvMaths::FVector3 GetLocalRight() const; + /** + * Create the user interface data if the transform doesn't have any yet + */ + void EnableUIData(); + + /** + * Discard the user interface data held by the transform + */ + void DisableUIData(); + + /** + * Check if the transform holds user interface data + */ + bool HasUIData() const; + + /** + * Check if the user interface data is in effect, meaning the owner has a canvas ancestor + */ + bool HasActiveUIData() const; + + /** + * Return the user interface data, if any + */ + const std::optional& GetUIData() const; + + /** + * Set the anchored position, in canvas units + * @param p_position + */ + void SetUIPosition(const OvMaths::FVector2& p_position); + + /** + * Return the anchored position, in canvas units + */ + const OvMaths::FVector2& GetUIPosition() const; + + /** + * Set the rotation around the canvas plane, in degrees, stored in the local rotation + * @param p_rotation + */ + void SetUIRotation(float p_rotation); + + /** + * Return the rotation around the canvas plane, in degrees + */ + float GetUIRotation() const; + + /** + * Set the scale applied on the canvas plane, stored in the local scale + * @param p_scale + */ + void SetUIScale(const OvMaths::FVector2& p_scale); + + /** + * Return the scale applied on the canvas plane + */ + OvMaths::FVector2 GetUIScale() const; + + /** + * Set the size in canvas units, an axis left to zero being derived from the element itself + * @param p_size + */ + void SetUISize(const OvMaths::FVector2& p_size); + + /** + * Return the size in canvas units, an axis left to zero being derived from the element itself + */ + const OvMaths::FVector2& GetUISize() const; + + /** + * Set the normalized pivot, clamped to the [-1, 1] range on both axes + * @param p_pivot + */ + void SetUIPivot(const OvMaths::FVector2& p_pivot); + + /** + * Return the normalized pivot + */ + const OvMaths::FVector2& GetUIPivot() const; + + /** + * Set the anchor preset used to resolve the element against its parent + * @param p_anchorPreset + */ + void SetUIAnchorPreset(EUIAnchorPreset p_anchorPreset); + + /** + * Return the anchor preset used to resolve the element against its parent + */ + EUIAnchorPreset GetUIAnchorPreset() const; + + /** + * Check if the horizontal anchored position can be edited, which isn't the case when the + * axis is stretched or driven by a parent layout + */ + bool IsHorizontalUIPositionEditable() const; + + /** + * Check if the vertical anchored position can be edited, which isn't the case when the + * axis is stretched or driven by a parent layout + */ + bool IsVerticalUIPositionEditable() const; + /** * Serialize the component * @param p_doc @@ -209,8 +342,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 +360,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..6716c62d4 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CCanvas.h @@ -0,0 +1,142 @@ +/** +* @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 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; + 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..8decd6ac8 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CHorizontalLayout.h @@ -0,0 +1,52 @@ +/** +* @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..557b304ad --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CImage.h @@ -0,0 +1,159 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#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; + + /** + * Defines whether the image keeps its intrinsic aspect ratio when resized + * @param p_preserveAspect + */ + void SetPreserveAspect(bool p_preserveAspect); + + /** + * Returns whether the image keeps its intrinsic aspect ratio when resized + */ + bool GetPreserveAspect() 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 SynchronizeTextureState() const; + void UpdateIntrinsicSize() const; + void RebuildMesh() const; + void RefreshMaterial(); + + private: + mutable OvRendering::Resources::Texture* m_texture = nullptr; + OvMaths::FVector4 m_tint = { 1.0f, 1.0f, 1.0f, 1.0f }; + mutable OvMaths::FVector2 m_intrinsicSize = { 100.0f, 100.0f }; + bool m_preserveAspect = false; + + mutable std::unique_ptr m_mesh; + std::unique_ptr m_material; + OvTools::Eventing::Event<> m_textureChangedEvent; + OvRendering::Resources::Texture* m_materialTexture = nullptr; + + mutable uint64_t m_textureManagerRevision = 0; + mutable uint64_t m_textureRevision = 0; + uint64_t m_materialManagerRevision = 0; + mutable bool m_textureReferenceDirty = true; + bool m_materialStateDirty = true; + mutable 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..448b6a27b --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CLayoutGroup.h @@ -0,0 +1,307 @@ +/** +* @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 +{ + /** + * 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); + + /** + * 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 child layout slots expand to use all available width + * @param p_forceExpandWidth + */ + void SetForceExpandWidth(bool p_forceExpandWidth); + + /** + * Returns whether child layout slots expand to use all available width + */ + bool GetForceExpandWidth() const; + + /** + * Sets whether child layout slots expand to use all available height + * @param p_forceExpandHeight + */ + void SetForceExpandHeight(bool p_forceExpandHeight); + + /** + * Returns whether child layout slots expand to use all available 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 resolved layout data for a direct child using an effective container size + * @param p_child + * @param p_containerSize + */ + std::optional GetChildLayout( + const ECS::Actor& p_child, + const OvMaths::FVector2& p_containerSize + ) 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; + 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 OvMaths::FVector2& p_containerSize) const; + const LayoutCache& GetResolvedLayout() const; + const LayoutCache& GetResolvedLayout(const OvMaths::FVector2& p_containerSize) const; + const LayoutCache& ResolveLayout( + const OvMaths::FVector2& p_containerSize, + LayoutCache& p_cache + ) 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 LayoutCache m_effectiveLayoutCache; + }; +} + +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..fd019a96c --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CText.h @@ -0,0 +1,221 @@ +/** +* @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 bool m_layoutAvailable = false; + mutable OvMaths::FVector2 m_lastLayoutUISize = OvMaths::FVector2::Zero; + mutable OvRendering::Resources::Font* m_layoutFont = nullptr; + mutable uint64_t m_layoutFontRevision = 0; + mutable uint64_t m_layoutFontManagerRevision = 0; + 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; + uint64_t m_materialFontManagerRevision = 0; + uint64_t m_materialManagerRevision = 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..83dcf468f --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/CVerticalLayout.h @@ -0,0 +1,52 @@ +/** +* @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/LayoutSolver.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/LayoutSolver.h new file mode 100644 index 000000000..e1f3d4168 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/LayoutSolver.h @@ -0,0 +1,81 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +#include +#include +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI +{ + /** + * Settings driving the resolution of a layout group + */ + struct LayoutSettings + { + 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; + }; + + /** + * Child submitted to a layout resolution, with the size it would take on its own + */ + struct LayoutChildInput + { + ECS::Actor* actor = nullptr; + OvMaths::FVector2 preferredSize = OvMaths::FVector2::Zero; + }; + + /** + * Placement resolved for a layout child, expressed in the centered layout coordinate system + */ + struct LayoutChildResult + { + ECS::Actor* actor = nullptr; + OvMaths::FVector2 offset = OvMaths::FVector2::Zero; + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + bool valid = false; + }; + + /** + * Container size and child placements produced by a layout resolution + */ + struct LayoutResult + { + OvMaths::FVector2 size = OvMaths::FVector2::Zero; + std::vector children; + }; + + /** + * Arranges layout children along an axis, distributing the leftover space when requested + */ + class LayoutSolver + { + public: + /** + * Resolve the container size and the placement of every child + * @param p_settings + * @param p_children + */ + static LayoutResult Solve( + const LayoutSettings& 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/UIInspectorUtils.h b/Sources/OvCore/include/OvCore/ECS/Components/UI/UIInspectorUtils.h new file mode 100644 index 000000000..43a603751 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/UIInspectorUtils.h @@ -0,0 +1,21 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI::UIInspectorUtils +{ + /** + * Draws a message when the given actor isn't part of a canvas hierarchy, and therefore isn't rendered + * @param p_root + * @param p_owner + */ + void DrawCanvasRequirement(OvUI::Internal::WidgetContainer& p_root, const ECS::Actor& p_owner); +} 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..cf8f7f265 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/UI/UITransformResolver.h @@ -0,0 +1,91 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include + +namespace OvCore::ECS { class Actor; } + +namespace OvCore::ECS::Components::UI::UITransformResolver +{ + /** + * Convert a serialized value to an anchor preset, falling back to CENTER when out of range + * @param p_value + */ + CTransform::EUIAnchorPreset ToAnchorPreset(int p_value); + + /** + * Return the normalized offset applied to the parent size for the given anchor preset + * @param p_anchorPreset + */ + OvMaths::FVector2 GetAnchorRatio(CTransform::EUIAnchorPreset p_anchorPreset); + + /** + * Check if the given anchor preset stretches the element horizontally + * @param p_anchorPreset + */ + bool IsHorizontalStretch(CTransform::EUIAnchorPreset p_anchorPreset); + + /** + * Check if the given anchor preset stretches the element vertically + * @param p_anchorPreset + */ + bool IsVerticalStretch(CTransform::EUIAnchorPreset p_anchorPreset); + + /** + * Check if the horizontal anchored position is editable for the given anchor preset + * @param p_anchorPreset + */ + bool IsHorizontalPositionEditable(CTransform::EUIAnchorPreset p_anchorPreset); + + /** + * Check if the vertical anchored position is editable for the given anchor preset + * @param p_anchorPreset + */ + bool IsVerticalPositionEditable(CTransform::EUIAnchorPreset p_anchorPreset); + + /** + * Return the closest actor holding a canvas, the given actor included, or nullptr + * @param p_owner + */ + ECS::Actor* FindCanvasOwner(ECS::Actor& p_owner); + + /** + * Return the closest actor holding a canvas, the given actor included, or nullptr + * @param p_owner + */ + const ECS::Actor* FindCanvasOwner(const ECS::Actor& p_owner); + + /** + * Return the closest ancestor holding a canvas, excluding the given actor, or nullptr + * @param p_owner + */ + const ECS::Actor* FindActiveCanvasOwner(const ECS::Actor& p_owner); + + /** + * Check if the given actor has a canvas ancestor, meaning its user interface data is in effect + * @param p_owner + */ + bool HasActiveUIData(const ECS::Actor& p_owner); + + /** + * Check if the given actor is a direct child of a layout group + * @param p_owner + */ + bool IsDrivenByLayout(const ECS::Actor& p_owner); + + /** + * Return the transform size, falling back to the element size on any axis left to zero + * @param p_transform + * @param p_elementSize + */ + OvMaths::FVector2 GetEffectiveSize( + const CTransform& p_transform, + const OvMaths::FVector2& p_elementSize + ); +} 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/EngineBufferRenderFeature.h b/Sources/OvCore/include/OvCore/Rendering/EngineBufferRenderFeature.h index 6a9c1ce6f..5dc4b8646 100644 --- a/Sources/OvCore/include/OvCore/Rendering/EngineBufferRenderFeature.h +++ b/Sources/OvCore/include/OvCore/Rendering/EngineBufferRenderFeature.h @@ -7,8 +7,8 @@ #pragma once #include -#include -#include +#include +#include #include @@ -44,8 +44,25 @@ namespace OvCore::Rendering virtual void OnEndFrame() override; virtual void OnBeforeDraw(OvRendering::Data::PipelineState& p_pso, const OvRendering::Entities::Drawable& p_drawable) override; + private: + struct CameraData + { + OvMaths::FMatrix4 viewMatrix; + OvMaths::FMatrix4 projectionMatrix; + OvMaths::FVector3 position; + }; + static_assert( + sizeof(CameraData) == sizeof(OvMaths::FMatrix4) * 2 + sizeof(OvMaths::FVector3), + "CameraData must match the engine UBO camera page" + ); + + void UploadCameraData(const CameraData& p_cameraData); + void RestoreFrameCamera(); + protected: std::chrono::high_resolution_clock::time_point m_startTime; std::unique_ptr m_engineBuffer; + CameraData m_frameCameraData; + bool m_cameraOverrideActive = false; }; } 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 020d2fec8..80d492a03 100644 --- a/Sources/OvCore/include/OvCore/Rendering/SceneRenderer.h +++ b/Sources/OvCore/include/OvCore/Rendering/SceneRenderer.h @@ -6,11 +6,15 @@ #pragma once +#include #include +#include +#include #include #include +#include #include #include #include @@ -23,6 +27,8 @@ namespace OvCore::Rendering { + namespace UIRenderingUtils { class UIFrameResolver; } + /** * Extension of the CompositeRenderer adding support for the scene system (parsing/drawing entities) */ @@ -39,7 +45,7 @@ namespace OvCore::Rendering struct DrawOrder { const int order; - const uintptr_t materialKey; + const OvRendering::Data::Material* materialKey; const float distance; /** @@ -54,7 +60,7 @@ namespace OvCore::Rendering { if (materialKey != p_other.materialKey) { - return materialKey < p_other.materialKey; + return std::less{}(materialKey, p_other.materialKey); } } @@ -86,11 +92,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 +120,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..1f2d6a604 --- /dev/null +++ b/Sources/OvCore/include/OvCore/Rendering/UIRenderingUtils.h @@ -0,0 +1,324 @@ +/** +* @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 CCanvas; } + +namespace OvCore::Rendering::UIRenderingUtils +{ + /** + * Canvas resolved against a render size, holding the matrices and scales used to draw its content + */ + 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; + }; + + /** + * User interface element resolved against its parent frame and its canvas + */ + 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; + OvMaths::FVector2 xPositionDirection = { 1.0f, 0.0f }; + OvMaths::FVector2 yPositionDirection = { 0.0f, 1.0f }; + OvMaths::FVector3 xWorldAxis = OvMaths::FVector3::Right; + OvMaths::FVector3 yWorldAxis = OvMaths::FVector3::Up; + float canvasScale = 1.0f; + float worldScale = 1.0f; + float unitsScale = 1.0f; + bool widthDriven = false; + bool heightDriven = false; + bool screenSpace = false; + }; + + /** + * Placement used to draw and manipulate the editor gizmo of a user interface element + */ + struct ResolvedUIGizmoTransform + { + OvMaths::FVector3 position = OvMaths::FVector3::Zero; + OvMaths::FQuaternion rotation = OvMaths::FQuaternion::Identity; + OvMaths::FVector2 xPositionDirection = { 1.0f, 0.0f }; + OvMaths::FVector2 yPositionDirection = { 0.0f, 1.0f }; + OvMaths::FVector3 xWorldAxis = OvMaths::FVector3::Right; + OvMaths::FVector3 yWorldAxis = OvMaths::FVector3::Up; + bool screenSpace = false; + }; + + /** + * Resolves canvases and user interface elements for a given render size, caching the results + * for the lifetime of the resolver + */ + class UIFrameResolver + { + public: + /** + * Constructor + * @param p_renderSize + * @param p_screenSpace + */ + UIFrameResolver( + const OvMaths::FVector2& p_renderSize = { 1.0f, 1.0f }, + bool p_screenSpace = true + ); + + /** + * Return the render size the resolver works against + */ + const OvMaths::FVector2& GetRenderSize() const; + + /** + * Return true when elements are resolved in screen space instead of world space + */ + bool IsScreenSpace() const; + + /** + * Create the orthographic projection matching the render size + * @param p_near + * @param p_far + */ + OvMaths::FMatrix4 CreateProjectionMatrix( + float p_near = -1.0f, + float p_far = 1.0f + ) const; + + /** + * Return the intrinsic size of the element held by the given actor + * @param p_actor + */ + OvMaths::FVector2 GetElementSize(const OvCore::ECS::Actor& p_actor) const; + + /** + * Resolve the canvas held by the given actor, returning false when it has none or when + * the resulting canvas size is degenerate + * @param p_actor + * @param p_outCanvas + */ + bool ResolveCanvas( + const OvCore::ECS::Actor& p_actor, + ResolvedUICanvas& p_outCanvas + ) const; + + /** + * Resolve the given actor as a user interface element using an explicit element size, + * returning false when it isn't under a canvas + * @param p_actor + * @param p_elementSize + * @param p_outElement + */ + bool ResolveElement( + const OvCore::ECS::Actor& p_actor, + const OvMaths::FVector2& p_elementSize, + ResolvedUIElement& p_outElement + ) const; + + /** + * Resolve the given actor as a user interface element using its own element size, + * returning false when it isn't under a canvas + * @param p_actor + * @param p_outElement + */ + 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 OvMaths::FVector2& p_parentSize + ) 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; + }; + + /** + * Return the given canvas size with both axes kept finite and at least one unit wide + * @param p_canvasSize + */ + OvMaths::FVector2 ClampCanvasSize(const OvMaths::FVector2& p_canvasSize); + + /** + * Create the orthographic projection matching the given render size + * @param p_renderSize + * @param p_near + * @param p_far + */ + OvMaths::FMatrix4 CreateUIProjectionMatrix( + const OvMaths::FVector2& p_renderSize, + float p_near = -1.0f, + float p_far = 1.0f + ); + + /** + * Return the size, in canvas units, covered by the given canvas + * @param p_canvas + * @param p_renderSize + */ + OvMaths::FVector2 GetCanvasSize( + const OvCore::ECS::Components::UI::CCanvas& p_canvas, + const OvMaths::FVector2& p_renderSize + ); + + /** + * Return the scale applied by the given canvas for the provided render size + * @param p_canvas + * @param p_renderSize + */ + float GetCanvasScale( + const OvCore::ECS::Components::UI::CCanvas& p_canvas, + const OvMaths::FVector2& p_renderSize + ); + + /** + * Return the canvas the given actor belongs to, or nullptr + * @param p_owner + */ + const OvCore::ECS::Components::UI::CCanvas* FindCanvas(const OvCore::ECS::Actor& p_owner); + + /** + * Return the closest actor holding a canvas, the given actor included, or nullptr + * @param p_owner + */ + OvCore::ECS::Actor* FindCanvasOwner(OvCore::ECS::Actor& p_owner); + + /** + * Return the closest actor holding a canvas, the given actor included, or nullptr + * @param p_owner + */ + const OvCore::ECS::Actor* FindCanvasOwner(const OvCore::ECS::Actor& p_owner); + + /** + * Return the size of the canvas the given actor belongs to, falling back to the render size + * @param p_owner + * @param p_renderSize + */ + OvMaths::FVector2 GetCanvasSize( + const OvCore::ECS::Actor& p_owner, + const OvMaths::FVector2& p_renderSize + ); + + /** + * Return the intrinsic size of the user interface component held by the given actor, + * falling back to the Transform user interface size + * @param p_owner + * @param p_renderSize + */ + OvMaths::FVector2 GetElementSize( + const OvCore::ECS::Actor& p_owner, + const OvMaths::FVector2& p_renderSize + ); + + /** + * Return the scale converting canvas units to world units for the given rendering mode + * @param p_screenSpace + */ + float GetUIWorldScale(bool p_screenSpace); + + /** + * Transform a point expressed on the canvas plane by the given matrix + * @param p_matrix + * @param p_point + */ + OvMaths::FVector3 TransformUIPoint( + const OvMaths::FMatrix4& p_matrix, + const OvMaths::FVector2& p_point + ); + + /** + * Return the transformed pivot point of the given resolved element + * @param p_element + */ + OvMaths::FVector3 TransformUIElementPivot(const ResolvedUIElement& p_element); + + /** + * Resolve the gizmo placement of the given actor, returning false for canvas owners and for + * actors that cannot be resolved as user interface elements + * @param p_frameResolver + * @param p_actor + * @param p_outTransform + */ + bool ResolveUIGizmoTransform( + const UIFrameResolver& p_frameResolver, + const OvCore::ECS::Actor& p_actor, + ResolvedUIGizmoTransform& p_outTransform + ); + +} diff --git a/Sources/OvCore/include/OvCore/ResourceManagement/AResourceManager.h b/Sources/OvCore/include/OvCore/ResourceManagement/AResourceManager.h index c034722df..ae40445dc 100644 --- a/Sources/OvCore/include/OvCore/ResourceManagement/AResourceManager.h +++ b/Sources/OvCore/include/OvCore/ResourceManagement/AResourceManager.h @@ -6,7 +6,9 @@ #pragma once +#include #include +#include #include #include @@ -50,6 +52,17 @@ namespace OvCore::ResourceManagement */ bool IsResourceRegistered(const std::filesystem::path& p_path); + /** + * Returns true if the provided instance is currently owned by this manager + * @param p_resource + */ + bool ContainsResource(const T* p_resource) const; + + /** + * Returns a monotonic revision changed whenever the resource registry is modified + */ + uint64_t GetResourcesRevision() const; + /** * Destroy and unregister every resources */ @@ -113,7 +126,8 @@ namespace OvCore::ResourceManagement inline static std::filesystem::path __ENGINE_ASSETS_PATH; std::unordered_map m_resources; + uint64_t m_resourcesRevision = 0; }; } -#include "OvCore/ResourceManagement/AResourceManager.inl" \ No newline at end of file +#include "OvCore/ResourceManagement/AResourceManager.inl" diff --git a/Sources/OvCore/include/OvCore/ResourceManagement/AResourceManager.inl b/Sources/OvCore/include/OvCore/ResourceManagement/AResourceManager.inl index efe522d70..788fa44a4 100644 --- a/Sources/OvCore/include/OvCore/ResourceManagement/AResourceManager.inl +++ b/Sources/OvCore/include/OvCore/ResourceManagement/AResourceManager.inl @@ -70,6 +70,7 @@ namespace OvCore::ResourceManagement if (auto resource = GetResource(p_path, false); resource) { ReloadResource(resource, p_path); + ++m_resourcesRevision; } } @@ -79,13 +80,43 @@ namespace OvCore::ResourceManagement return m_resources.find(NormalizeKey(p_path)) != m_resources.end(); } + template + inline bool AResourceManager::ContainsResource(const T* p_resource) const + { + if (!p_resource) + { + return false; + } + + return std::any_of( + m_resources.begin(), + m_resources.end(), + [p_resource](const auto& p_entry) + { + return p_entry.second == p_resource; + } + ); + } + + template + inline uint64_t AResourceManager::GetResourcesRevision() const + { + return m_resourcesRevision; + } + template inline void AResourceManager::UnloadResources() { + if (m_resources.empty()) + { + return; + } + for (auto&[key, value] : m_resources) DestroyResource(value); m_resources.clear(); + ++m_resourcesRevision; } template @@ -99,6 +130,7 @@ namespace OvCore::ResourceManagement } m_resources[key] = p_instance; + ++m_resourcesRevision; return p_instance; } @@ -106,7 +138,10 @@ namespace OvCore::ResourceManagement template inline void AResourceManager::UnregisterResource(const std::filesystem::path & p_path) { - m_resources.erase(NormalizeKey(p_path)); + if (m_resources.erase(NormalizeKey(p_path)) != 0) + { + ++m_resourcesRevision; + } } template @@ -170,4 +205,4 @@ namespace OvCore::ResourceManagement return path.lexically_normal(); } -} \ No newline at end of file +} 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/include/OvCore/SceneSystem/Scene.h b/Sources/OvCore/include/OvCore/SceneSystem/Scene.h index 36bf16011..affbd1778 100644 --- a/Sources/OvCore/include/OvCore/SceneSystem/Scene.h +++ b/Sources/OvCore/include/OvCore/SceneSystem/Scene.h @@ -206,6 +206,13 @@ namespace OvCore::SceneSystem */ std::vector& GetActors(); + /** + * Moves an actor to the end of the scene order. + * @param p_actor + * @return True when the actor belongs to this scene + */ + bool MoveActorToEnd(ECS::Actor& p_actor); + /** * Return the fast access components data structure */ 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..71839bf43 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,167 @@ 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()) && + !OvCore::ECS::Components::UI::UITransformResolver::IsDrivenByLayout(owner); +} + +bool OvCore::ECS::Components::CTransform::IsVerticalUIPositionEditable() const +{ + return + IsVerticalUIPositionEditable(GetUIAnchorPreset()) && + !OvCore::ECS::Components::UI::UITransformResolver::IsDrivenByLayout(owner); +} + 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 +369,210 @@ 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(); + + if (IsHorizontalUIPositionEditable()) + { + position.x = p_value[0]; + } + + if (IsVerticalUIPositionEditable()) + { + position.y = p_value[1]; + } + + SetUIPosition(position); + }); + + OvCore::Helpers::GUIDrawer::CreateTitle(p_root, "Size"); + auto& size = p_root.CreateWidget>( + OvCore::Helpers::GUIDrawer::GetDataType(), + kMinimumSize, + OvCore::Helpers::GUIDrawer::_MAX_FLOAT, + 0.0f, + 1.0f, + "", + OvCore::Helpers::GUIDrawer::GetFormat() + ); + auto& sizeDispatcher = size.AddPlugin>>(); + sizeDispatcher.RegisterGatherer([this]() + { + const auto value = GetUISize(); + return std::array{ value.x, value.y }; + }); + sizeDispatcher.RegisterProvider([this](std::array p_value) + { + auto value = GetUISize(); + const auto anchorPreset = GetUIAnchorPreset(); + const bool drivenByLayout = OvCore::ECS::Components::UI::UITransformResolver::IsDrivenByLayout(owner); + + if (drivenByLayout || !OvCore::ECS::Components::UI::UITransformResolver::IsHorizontalStretch(anchorPreset)) + { + value.x = p_value[0]; + } + if (drivenByLayout || !OvCore::ECS::Components::UI::UITransformResolver::IsVerticalStretch(anchorPreset)) + { + value.y = p_value[1]; + } + + SetUISize(value); + }); + + const auto updateTransform2DEditability = [ + this, + anchoredPositionWidget = &anchoredPosition, + sizeWidget = &size + ]() + { + const auto anchorPreset = GetUIAnchorPreset(); + const bool drivenByLayout = OvCore::ECS::Components::UI::UITransformResolver::IsDrivenByLayout(owner); + + anchoredPositionWidget->disabledComponents = { + !IsHorizontalUIPositionEditable(), + !IsVerticalUIPositionEditable() + }; + sizeWidget->disabledComponents = { + !drivenByLayout && OvCore::ECS::Components::UI::UITransformResolver::IsHorizontalStretch(anchorPreset), + !drivenByLayout && OvCore::ECS::Components::UI::UITransformResolver::IsVerticalStretch(anchorPreset) + }; + }; + updateTransform2DEditability(); + + 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, updateTransform2DEditability](int p_choice) + { + SetUIAnchorPreset(ToUIAnchorPreset(p_choice)); + updateTransform2DEditability(); + }; + } + 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..f8bb3487b --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CCanvas.cpp @@ -0,0 +1,270 @@ +/** +* @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 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::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::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("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::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..ee4697d94 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CImage.cpp @@ -0,0 +1,362 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#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 }; + } +} + +OvCore::ECS::Components::UI::CImage::CImage(ECS::Actor& p_owner) : +AComponent(p_owner) +{ + m_textureChangedEvent += [this] + { + m_textureReferenceDirty = true; + m_materialTextureDirty = true; + SynchronizeTextureState(); + }; + + 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) +{ + SynchronizeTextureState(); + + if (m_texture == p_texture) + { + return; + } + + m_texture = p_texture; + m_textureReferenceDirty = true; + m_materialTextureDirty = true; + SynchronizeTextureState(); +} + +OvRendering::Resources::Texture* OvCore::ECS::Components::UI::CImage::GetTexture() const +{ + SynchronizeTextureState(); + 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 +{ + SynchronizeTextureState(); + 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; +} + +void OvCore::ECS::Components::UI::CImage::SetPreserveAspect(bool p_preserveAspect) +{ + m_preserveAspect = p_preserveAspect; +} + +bool OvCore::ECS::Components::UI::CImage::GetPreserveAspect() const +{ + return m_preserveAspect; +} + +OvRendering::Resources::Mesh& OvCore::ECS::Components::UI::CImage::GetMesh() const +{ + SynchronizeTextureState(); + 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) +{ + SynchronizeTextureState(); + Helpers::Serializer::SerializeTexture(p_doc, p_node, "texture", m_texture); + Helpers::Serializer::SerializeVec4(p_doc, p_node, "tint", m_tint); + Helpers::Serializer::SerializeBoolean(p_doc, p_node, "preserve_aspect", m_preserveAspect); +} + +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); + } + + if (p_node->FirstChildElement("preserve_aspect")) + { + auto preserveAspect = m_preserveAspect; + Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "preserve_aspect", preserveAspect); + SetPreserveAspect(preserveAspect); + } +} + +void OvCore::ECS::Components::UI::CImage::OnInspector(OvUI::Internal::WidgetContainer& p_root) +{ + UIInspectorUtils::DrawCanvasRequirement(p_root, owner); + + SynchronizeTextureState(); + Helpers::GUIDrawer::DrawTexture(p_root, "Texture", m_texture, &m_textureChangedEvent); + Helpers::GUIDrawer::DrawBoolean( + p_root, + "Preserve Aspect", + [this]() { return GetPreserveAspect(); }, + [this](bool p_value) { SetPreserveAspect(p_value); } + ); + + 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 +{ + const auto size = m_intrinsicSize; + + 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::SynchronizeTextureState() const +{ + auto& textureManager = Global::ServiceLocator::Get(); + const auto managerRevision = textureManager.GetResourcesRevision(); + const bool registryChanged = managerRevision != m_textureManagerRevision; + bool textureStateChanged = m_textureReferenceDirty; + + if (m_texture && (m_textureReferenceDirty || registryChanged) && !textureManager.ContainsResource(m_texture)) + { + m_texture = nullptr; + textureStateChanged = true; + } + + m_textureReferenceDirty = false; + m_textureManagerRevision = managerRevision; + + const uint64_t textureRevision = m_texture ? m_texture->GetRevision() : 0; + textureStateChanged |= textureRevision != m_textureRevision; + m_textureRevision = textureRevision; + + if (textureStateChanged) + { + m_materialTextureDirty = true; + UpdateIntrinsicSize(); + RebuildMesh(); + } +} + +void OvCore::ECS::Components::UI::CImage::UpdateIntrinsicSize() const +{ + const auto defaultSize = GetDefaultImageSize(); + m_intrinsicSize = defaultSize; + + if (!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() +{ + SynchronizeTextureState(); + auto& materialManager = Global::ServiceLocator::Get(); + + if (m_materialManagerRevision != materialManager.GetResourcesRevision()) + { + m_materialStateDirty = true; + } + + if (!m_material) + { + m_material = std::make_unique(); + m_materialStateDirty = true; + m_materialTextureDirty = true; + m_materialTintDirty = true; + } + + 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 : + materialManager.GetResource(imageMaterialPath); + m_materialManagerRevision = materialManager.GetResourcesRevision(); + + 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..b19f04d4b --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CLayoutGroup.cpp @@ -0,0 +1,707 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + constexpr float 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; + } + + OvMaths::FVector2 ClampLayoutSize(const OvMaths::FVector2& p_size) + { + return { + std::isfinite(p_size.x) ? std::max(p_size.x, 0.0f) : 0.0f, + std::isfinite(p_size.y) ? std::max(p_size.y, 0.0f) : 0.0f + }; + } + + 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; + } + } + + OvMaths::FVector2 ToLayoutSpace(const OvMaths::FVector2& p_size, const OvMaths::FVector2& p_scale) + { + return { p_size.x * p_scale.x, p_size.y * p_scale.y }; + } + + OvMaths::FVector2 ToElementSpace(const OvMaths::FVector2& p_size, const OvMaths::FVector2& p_scale) + { + return { p_size.x / p_scale.x, p_size.y / p_scale.y }; + } + + // Layout slots reserve the space occupied on screen, which includes the child scale + 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; + } + + const auto scale = p_child.transform.GetUIScale(); + + 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 ToLayoutSpace(size, scale); + } + } + + if (hasElementSize && elementSize.x > 0.0f && elementSize.y > 0.0f) + { + return ToLayoutSpace(elementSize, scale); + } + + return std::nullopt; + } + + OvCore::ECS::Components::UI::LayoutSettings CreateLayoutSettings( + const OvCore::ECS::Components::UI::CLayoutGroup& p_layout, + const OvMaths::FVector2& p_containerSize + ) + { + 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_containerSize + }; + } +} + +OvCore::ECS::Components::UI::CLayoutGroup::CLayoutGroup(ECS::Actor& p_owner) : +AComponent(p_owner) +{ + owner.transform.EnableUIData(); +} + +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::optional OvCore::ECS::Components::UI::CLayoutGroup::GetChildLayout( + const ECS::Actor& p_child, + const OvMaths::FVector2& p_containerSize +) const +{ + if (p_child.GetParent() != &owner) + { + return std::nullopt; + } + + for (const auto& childLayout : GetResolvedLayout(p_containerSize).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 OvMaths::FVector2& p_containerSize +) const +{ + LayoutCacheInput input; + const auto settings = CreateLayoutSettings(*this, p_containerSize); + + 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 + }; + + 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 +{ + return ResolveLayout(owner.transform.GetUISize(), m_layoutCache); +} + +const OvCore::ECS::Components::UI::CLayoutGroup::LayoutCache& OvCore::ECS::Components::UI::CLayoutGroup::GetResolvedLayout( + const OvMaths::FVector2& p_containerSize +) const +{ + const auto containerSize = ClampLayoutSize(p_containerSize); + const auto& transformSize = owner.transform.GetUISize(); + if (containerSize.x == transformSize.x && containerSize.y == transformSize.y) + { + return GetResolvedLayout(); + } + + return ResolveLayout(containerSize, m_effectiveLayoutCache); +} + +const OvCore::ECS::Components::UI::CLayoutGroup::LayoutCache& OvCore::ECS::Components::UI::CLayoutGroup::ResolveLayout( + const OvMaths::FVector2& p_containerSize, + LayoutCache& p_cache +) const +{ + const auto input = BuildLayoutCacheInput(p_containerSize); + + if (p_cache.valid && HasSameLayoutSignature(p_cache.signature, input.signature)) + { + return p_cache; + } + + 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 = LayoutSolver::Solve( + CreateLayoutSettings(*this, p_containerSize), + layoutChildren + ); + + p_cache.valid = true; + p_cache.signature = input.signature; + p_cache.size = layoutResult.size; + p_cache.children.clear(); + p_cache.children.reserve(layoutResult.children.size()); + + for (const auto& child : layoutResult.children) + { + const auto scale = child.actor ? child.actor->transform.GetUIScale() : OvMaths::FVector2::One; + + p_cache.children.push_back({ + .actor = child.actor, + .offset = child.offset, + .size = ToElementSpace(child.size, scale), + .hasDirectWidth = GetControlChildrenWidth(), + .hasDirectHeight = GetControlChildrenHeight(), + .valid = child.valid + }); + } + + return p_cache; +} + +void OvCore::ECS::Components::UI::CLayoutGroup::InvalidateLayoutCache() const +{ + m_layoutCache.valid = false; + m_layoutCache.children.clear(); + m_effectiveLayoutCache.valid = false; + m_effectiveLayoutCache.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.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) +{ + UIInspectorUtils::DrawCanvasRequirement(p_root, owner); + + 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..d2d605762 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/CText.cpp @@ -0,0 +1,589 @@ +/** +* @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 +#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) +{ + UIInspectorUtils::DrawCanvasRequirement(p_root, owner); + + 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 +{ + auto* font = GetFont(); + const auto fontManagerRevision = Global::ServiceLocator::Get().GetResourcesRevision(); + const uint64_t fontRevision = font ? font->GetRevision() : 0; + const bool fontChanged = + font != m_layoutFont || + fontRevision != m_layoutFontRevision || + fontManagerRevision != m_layoutFontManagerRevision; + + if (!m_layoutDirty && !fontChanged && IsSameSize(m_lastLayoutUISize, p_uiSize)) + { + return; + } + + m_meshDirty = true; + m_layoutFont = font; + m_layoutFontRevision = fontRevision; + m_layoutFontManagerRevision = fontManagerRevision; + + if (!font) + { + m_layout = {}; + m_size = OvMaths::FVector2::Zero; + m_lastLayoutUISize = p_uiSize; + m_layoutDirty = false; + m_layoutAvailable = false; + m_meshDirty = true; + return; + } + + m_layoutDirty = false; + m_layoutAvailable = true; + 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 +{ + RebuildLayout(p_uiSize); + + if (!m_meshDirty && IsSameSize(m_lastMeshUISize, p_uiSize)) + { + return; + } + + if (!m_layoutAvailable) + { + m_mesh.reset(); + m_meshDirty = false; + m_lastMeshUISize = p_uiSize; + 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() +{ + auto& fontManager = Global::ServiceLocator::Get(); + auto& materialManager = Global::ServiceLocator::Get(); + + 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 : + materialManager.GetResource(textMaterialPath, false); + auto* currentShader = defaultMaterial && defaultMaterial->HasShader() ? defaultMaterial->GetShader() : nullptr; + auto* currentFont = m_fontPath.empty() || m_fontPath == "?" ? + nullptr : + fontManager.GetResource(m_fontPath, false); + + if ( + materialManager.GetResourcesRevision() != m_materialManagerRevision || + fontManager.GetResourcesRevision() != m_materialFontManagerRevision || + 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 : + materialManager.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; + m_materialFontManagerRevision = fontManager.GetResourcesRevision(); + m_materialManagerRevision = materialManager.GetResourcesRevision(); + + 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/LayoutSolver.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/LayoutSolver.cpp new file mode 100644 index 000000000..d7113402e --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/LayoutSolver.cpp @@ -0,0 +1,376 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + constexpr float kMinimumLayoutSize = 1.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()); + + struct LayoutPadding + { + float left = 0.0f; + float right = 0.0f; + float top = 0.0f; + float bottom = 0.0f; + }; + + 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); + } + + // Spacing and padding are kept whole so that slots always land on integer offsets + float ToSpacing(float p_spacing) + { + return std::trunc(std::clamp(KeepFinite(p_spacing, kMinimumSpacing), kMinimumSpacing, kMaximumSpacing)); + } + + float ToPaddingAxis(float p_padding) + { + return std::trunc(std::clamp(KeepFinite(p_padding, kMinimumPadding), kMinimumPadding, kMaximumPadding)); + } + + LayoutPadding ToPadding(const OvMaths::FVector4& p_padding) + { + return { + .left = ToPaddingAxis(p_padding.x), + .right = ToPaddingAxis(p_padding.y), + .top = ToPaddingAxis(p_padding.z), + .bottom = ToPaddingAxis(p_padding.w) + }; + } + + bool IsHorizontal(const OvCore::ECS::Components::UI::LayoutSettings& p_settings) + { + return p_settings.direction == OvCore::ECS::Components::UI::CLayoutGroup::EDirection::HORIZONTAL; + } + + float GetHorizontalAlignmentFactor( + OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment p_alignment + ) + { + switch (p_alignment) + { + case OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment::LEFT: + return 0.0f; + case OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment::RIGHT: + return 1.0f; + case OvCore::ECS::Components::UI::CLayoutGroup::EHorizontalAlignment::CENTER: + default: + return 0.5f; + } + } + + float GetVerticalAlignmentFactor( + OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment p_alignment + ) + { + switch (p_alignment) + { + case OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment::TOP: + return 0.0f; + case OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment::BOTTOM: + return 1.0f; + case OvCore::ECS::Components::UI::CLayoutGroup::EVerticalAlignment::CENTER: + default: + return 0.5f; + } + } + + OvMaths::FVector2 ToChildOffset( + const OvMaths::FVector2& p_childTopLeft, + const OvMaths::FVector2& p_childSize, + const OvMaths::FVector2& p_layoutSize + ) + { + 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; + + return { + centerX - halfSize.x, + halfSize.y - centerY + }; + } + + // Raises the smallest slots first until every slot shares the same size, then splits what is + // left evenly. Slots are never shrunk below the size they requested. + void ExpandAlongMainAxis(std::vector& p_sizes, float p_availableSize) + { + if (p_sizes.empty()) + { + return; + } + + float contentSize = 0.0f; + for (const auto size : p_sizes) + { + contentSize += size; + } + + float remainingSize = p_availableSize - contentSize; + if (remainingSize <= 0.0f) + { + return; + } + + std::vector sortedSizes = p_sizes; + std::sort(sortedSizes.begin(), sortedSizes.end()); + + float level = sortedSizes.back(); + for (size_t i = 0; i < sortedSizes.size(); ++i) + { + const auto slotCount = static_cast(i + 1); + const bool isLastLevel = i + 1 == sortedSizes.size(); + const float levelCost = isLastLevel ? + remainingSize : + (sortedSizes[i + 1] - sortedSizes[i]) * slotCount; + + if (remainingSize <= levelCost) + { + level = sortedSizes[i] + remainingSize / slotCount; + break; + } + + remainingSize -= levelCost; + } + + for (auto& size : p_sizes) + { + size = std::max(size, level); + } + } + + OvMaths::FVector2 CalculateContentSize( + const OvCore::ECS::Components::UI::LayoutSettings& p_settings, + const std::vector& p_children, + const LayoutPadding& p_padding, + float p_spacing + ) + { + const bool horizontal = IsHorizontal(p_settings); + float mainAxisSize = 0.0f; + float crossAxisSize = 0.0f; + + for (const auto& child : p_children) + { + const float childWidth = ClampNonNegative(child.preferredSize.x); + const float childHeight = ClampNonNegative(child.preferredSize.y); + + mainAxisSize += horizontal ? childWidth : childHeight; + crossAxisSize = std::max(crossAxisSize, horizontal ? childHeight : childWidth); + } + + if (p_children.size() > 1) + { + mainAxisSize += p_spacing * static_cast(p_children.size() - 1); + } + + const float horizontalPadding = p_padding.left + p_padding.right; + const float verticalPadding = p_padding.top + p_padding.bottom; + + return horizontal ? + OvMaths::FVector2{ mainAxisSize + horizontalPadding, crossAxisSize + verticalPadding } : + OvMaths::FVector2{ crossAxisSize + horizontalPadding, mainAxisSize + verticalPadding }; + } + + OvMaths::FVector2 GetLayoutSize( + const OvMaths::FVector2& p_containerSize, + const OvMaths::FVector2& p_contentSize + ) + { + return { + p_containerSize.x > 0.0f ? p_containerSize.x : std::max(p_contentSize.x, kMinimumLayoutSize), + p_containerSize.y > 0.0f ? p_containerSize.y : std::max(p_contentSize.y, kMinimumLayoutSize) + }; + } + + std::vector CalculateSlotSizes( + const OvCore::ECS::Components::UI::LayoutSettings& p_settings, + const std::vector& p_children, + const OvMaths::FVector2& p_layoutSize, + const LayoutPadding& p_padding, + float p_spacing + ) + { + const bool horizontal = IsHorizontal(p_settings); + const float availableWidth = std::max(p_layoutSize.x - (p_padding.left + p_padding.right), 0.0f); + const float availableHeight = std::max(p_layoutSize.y - (p_padding.top + p_padding.bottom), 0.0f); + + std::vector slotSizes; + slotSizes.reserve(p_children.size()); + + for (const auto& child : p_children) + { + slotSizes.push_back({ + ClampNonNegative(child.preferredSize.x), + ClampNonNegative(child.preferredSize.y) + }); + } + + const bool expandMainAxis = horizontal ? p_settings.forceExpandWidth : p_settings.forceExpandHeight; + const bool expandCrossAxis = horizontal ? p_settings.forceExpandHeight : p_settings.forceExpandWidth; + + if (expandMainAxis && !slotSizes.empty()) + { + const float totalSpacing = slotSizes.size() > 1 ? + p_spacing * static_cast(slotSizes.size() - 1) : + 0.0f; + const float availableMainAxisSize = std::max( + (horizontal ? availableWidth : availableHeight) - totalSpacing, + 0.0f + ); + + std::vector mainAxisSizes; + mainAxisSizes.reserve(slotSizes.size()); + + for (const auto& slotSize : slotSizes) + { + mainAxisSizes.push_back(horizontal ? slotSize.x : slotSize.y); + } + + ExpandAlongMainAxis(mainAxisSizes, availableMainAxisSize); + + for (size_t i = 0; i < slotSizes.size(); ++i) + { + float& mainAxisSize = horizontal ? slotSizes[i].x : slotSizes[i].y; + mainAxisSize = mainAxisSizes[i]; + } + } + + if (expandCrossAxis) + { + const float availableCrossAxisSize = horizontal ? availableHeight : availableWidth; + + for (auto& slotSize : slotSizes) + { + float& crossAxisSize = horizontal ? slotSize.y : slotSize.x; + crossAxisSize = std::max(crossAxisSize, availableCrossAxisSize); + } + } + + return slotSizes; + } + + std::vector ProjectSlotsToChildren( + const OvCore::ECS::Components::UI::LayoutSettings& p_settings, + const std::vector& p_children, + const std::vector& p_slotSizes, + const OvMaths::FVector2& p_layoutSize, + const LayoutPadding& p_padding, + float p_spacing + ) + { + using LayoutChildResult = OvCore::ECS::Components::UI::LayoutChildResult; + + const bool horizontal = IsHorizontal(p_settings); + const float horizontalAlignment = GetHorizontalAlignmentFactor(p_settings.horizontalAlignment); + const float verticalAlignment = GetVerticalAlignmentFactor(p_settings.verticalAlignment); + const float availableWidth = std::max(p_layoutSize.x - (p_padding.left + p_padding.right), 0.0f); + const float availableHeight = std::max(p_layoutSize.y - (p_padding.top + p_padding.bottom), 0.0f); + + float mainAxisContentSize = 0.0f; + for (const auto& slotSize : p_slotSizes) + { + mainAxisContentSize += horizontal ? slotSize.x : slotSize.y; + } + + if (p_slotSizes.size() > 1) + { + mainAxisContentSize += p_spacing * static_cast(p_slotSizes.size() - 1); + } + + const float mainAxisAvailableSize = horizontal ? availableWidth : availableHeight; + const float mainAxisAlignment = horizontal ? horizontalAlignment : verticalAlignment; + float mainAxisCursor = + (horizontal ? p_padding.left : p_padding.top) + + std::max(mainAxisAvailableSize - mainAxisContentSize, 0.0f) * mainAxisAlignment; + + std::vector results; + results.reserve(p_children.size()); + + for (size_t i = 0; i < p_children.size(); ++i) + { + const auto& slotSize = p_slotSizes[i]; + OvMaths::FVector2 slotTopLeft; + + if (horizontal) + { + slotTopLeft = { + mainAxisCursor, + p_padding.top + std::max(availableHeight - slotSize.y, 0.0f) * verticalAlignment + }; + mainAxisCursor += slotSize.x + p_spacing; + } + else + { + slotTopLeft = { + p_padding.left + std::max(availableWidth - slotSize.x, 0.0f) * horizontalAlignment, + mainAxisCursor + }; + mainAxisCursor += slotSize.y + p_spacing; + } + + const OvMaths::FVector2 childSize = { + p_settings.controlChildrenWidth ? slotSize.x : ClampNonNegative(p_children[i].preferredSize.x), + p_settings.controlChildrenHeight ? slotSize.y : ClampNonNegative(p_children[i].preferredSize.y) + }; + const OvMaths::FVector2 childTopLeft = { + slotTopLeft.x + std::max(slotSize.x - childSize.x, 0.0f) * horizontalAlignment, + slotTopLeft.y + std::max(slotSize.y - childSize.y, 0.0f) * verticalAlignment + }; + + results.push_back({ + .actor = p_children[i].actor, + .offset = ToChildOffset(childTopLeft, childSize, p_layoutSize), + .size = childSize, + .valid = p_children[i].actor && childSize.x > 0.0f && childSize.y > 0.0f + }); + } + + return results; + } +} + +OvCore::ECS::Components::UI::LayoutResult OvCore::ECS::Components::UI::LayoutSolver::Solve( + const LayoutSettings& p_settings, + const std::vector& p_children +) +{ + LayoutSettings settings = p_settings; + settings.containerSize.x = ClampNonNegative(settings.containerSize.x); + settings.containerSize.y = ClampNonNegative(settings.containerSize.y); + + const auto padding = ToPadding(settings.padding); + const float spacing = ToSpacing(settings.spacing); + const auto contentSize = CalculateContentSize(settings, p_children, padding, spacing); + + LayoutResult result; + result.size = GetLayoutSize(settings.containerSize, contentSize); + + const auto slotSizes = CalculateSlotSizes(settings, p_children, result.size, padding, spacing); + result.children = ProjectSlotsToChildren(settings, p_children, slotSizes, result.size, padding, spacing); + + return result; +} 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..1699c05c6 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/TextLayoutEngine.cpp @@ -0,0 +1,509 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + using CodePoint = uint32_t; + + constexpr CodePoint kReplacementCodePoint = 0xFFFD; + + 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 + }; + } + + std::vector DecodeUTF8(std::string_view p_text) + { + std::vector result; + result.reserve(p_text.size()); + + for (size_t index = 0; index < p_text.size();) + { + const auto firstByte = static_cast(p_text[index]); + if (firstByte <= 0x7F) + { + result.push_back(firstByte); + ++index; + continue; + } + + size_t sequenceLength = 0; + CodePoint codePoint = 0; + CodePoint minimumCodePoint = 0; + if (firstByte >= 0xC2 && firstByte <= 0xDF) + { + sequenceLength = 2; + codePoint = firstByte & 0x1F; + minimumCodePoint = 0x80; + } + else if (firstByte >= 0xE0 && firstByte <= 0xEF) + { + sequenceLength = 3; + codePoint = firstByte & 0x0F; + minimumCodePoint = 0x800; + } + else if (firstByte >= 0xF0 && firstByte <= 0xF4) + { + sequenceLength = 4; + codePoint = firstByte & 0x07; + minimumCodePoint = 0x10000; + } + + bool valid = sequenceLength > 0 && index + sequenceLength <= p_text.size(); + for (size_t offset = 1; valid && offset < sequenceLength; ++offset) + { + const auto continuationByte = static_cast(p_text[index + offset]); + valid = (continuationByte & 0xC0) == 0x80; + codePoint = (codePoint << 6) | (continuationByte & 0x3F); + } + + valid = + valid && + codePoint >= minimumCodePoint && + codePoint <= 0x10FFFF && + (codePoint < 0xD800 || codePoint > 0xDFFF); + + if (!valid) + { + result.push_back(kReplacementCodePoint); + ++index; + continue; + } + + result.push_back(codePoint); + index += sequenceLength; + } + + return result; + } + + bool IsSoftWrapWhitespace(CodePoint p_codePoint) + { + return p_codePoint == ' ' || p_codePoint == '\t'; + } + + float GetGlyphAdvance( + const OvRendering::Resources::Font& p_font, + const OvRendering::Resources::Font::Glyph* p_fallbackGlyph, + float p_fontSize, + float p_scale, + CodePoint p_codePoint + ) + { + const auto* glyph = p_font.GetGlyph(p_codePoint, 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, + const std::vector& 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::vector& p_output, + const OvRendering::Resources::Font& p_font, + const OvRendering::Resources::Font::Glyph* p_fallbackGlyph, + float p_fontSize, + float p_scale, + float p_maxWidth, + const std::vector& 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_output.insert(p_output.end(), p_text.begin() + p_begin, p_text.begin() + p_end); + 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.push_back('\n'); + p_lineWidth = 0.0f; + p_lineHasContent = false; + } + + p_output.push_back(p_text[index]); + p_lineWidth += characterWidth; + p_lineHasContent = true; + } + } + + std::vector WrapTextToWidth( + const std::vector& 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 p_text; + } + + std::vector output; + output.reserve(p_text.size()); + + float lineWidth = 0.0f; + float pendingWhitespaceWidth = 0.0f; + std::vector pendingWhitespace; + bool lineHasContent = false; + + for (size_t index = 0; index < p_text.size();) + { + const auto codePoint = p_text[index]; + if (codePoint == '\r') + { + ++index; + continue; + } + + if (codePoint == '\n') + { + output.push_back('\n'); + lineWidth = 0.0f; + pendingWhitespaceWidth = 0.0f; + pendingWhitespace.clear(); + lineHasContent = false; + ++index; + continue; + } + + if (IsSoftWrapWhitespace(codePoint)) + { + const size_t whitespaceBegin = index; + while (index < p_text.size() && IsSoftWrapWhitespace(p_text[index])) + { + ++index; + } + + if (lineHasContent) + { + pendingWhitespace.insert( + pendingWhitespace.end(), + p_text.begin() + whitespaceBegin, + p_text.begin() + index + ); + 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.push_back('\n'); + lineWidth = 0.0f; + lineHasContent = false; + } + else if (lineHasContent && !pendingWhitespace.empty()) + { + output.insert(output.end(), pendingWhitespace.begin(), pendingWhitespace.end()); + 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 lineAdvance = p_input.font->GetLineHeight(p_input.fontSize) * scale; + const float ascender = p_input.font->GetAscender(p_input.fontSize) * scale; + const float descender = p_input.font->GetDescender(p_input.fontSize) * scale; + + struct LineInfo + { + size_t firstGlyph = 0; + size_t lastGlyph = 0; + float minX = std::numeric_limits::max(); + float maxX = std::numeric_limits::lowest(); + float advance = 0.0f; + bool hasGeometry = false; + }; + + std::vector lines; + lines.push_back({}); + lines.back().firstGlyph = 0; + + float cursorX = 0.0f; + float baselineY = 0.0f; + float minY = std::numeric_limits::max(); + float maxY = std::numeric_limits::lowest(); + bool hasGeometry = false; + + const auto* fallbackGlyph = p_input.font->GetGlyph('?', p_input.fontSize); + const auto codePoints = DecodeUTF8(p_input.text); + const auto wrappedText = WrapTextToWidth( + codePoints, + *p_input.font, + fallbackGlyph, + p_input.fontSize, + scale, + p_input.uiSize.x + ); + + output.glyphs.reserve(wrappedText.size()); + + for (const auto codePoint : wrappedText) + { + if (codePoint == '\r') + { + continue; + } + + if (codePoint == '\n') + { + lines.back().lastGlyph = output.glyphs.size(); + lines.back().advance = cursorX; + lines.push_back({}); + lines.back().firstGlyph = output.glyphs.size(); + cursorX = 0.0f; + baselineY -= lineAdvance; + continue; + } + + const auto* glyph = p_input.font->GetGlyph(codePoint, 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; + + auto& line = lines.back(); + if (glyph->width > 0.0f && glyph->height > 0.0f) + { + output.glyphs.push_back({ + .left = x0, + .right = x1, + .bottom = bottomY, + .top = topY, + .uMin = glyph->uMin, + .uMax = glyph->uMax, + .vMin = glyph->vMin, + .vMax = glyph->vMax + }); + + hasGeometry = true; + minY = std::min(minY, bottomY); + maxY = std::max(maxY, topY); + 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; + line.advance = cursorX; + } + + lines.back().lastGlyph = output.glyphs.size(); + lines.back().advance = cursorX; + + float contentWidth = 0.0f; + for (const auto& line : lines) + { + const float lineMinX = line.hasGeometry ? std::min(line.minX, 0.0f) : 0.0f; + const float lineMaxX = line.hasGeometry ? std::max(line.maxX, line.advance) : line.advance; + contentWidth = std::max(contentWidth, std::max(lineMaxX - lineMinX, 0.0f)); + } + + const float logicalTop = ascender; + const float logicalBottom = -static_cast(lines.size() - 1) * lineAdvance + descender; + const float contentTop = hasGeometry ? std::max(logicalTop, maxY) : logicalTop; + const float contentBottom = hasGeometry ? std::min(logicalBottom, minY) : logicalBottom; + output.contentSize = { + contentWidth, + std::max(contentTop - contentBottom, 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 lineMinX = std::min(line.minX, 0.0f); + const float lineMaxX = std::max(line.maxX, line.advance); + const float lineWidth = std::max(lineMaxX - lineMinX, 0.0f); + const float lineCenterX = lineMinX + 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 = contentBottom + 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/UIInspectorUtils.cpp b/Sources/OvCore/src/OvCore/ECS/Components/UI/UIInspectorUtils.cpp new file mode 100644 index 000000000..472727407 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/UIInspectorUtils.cpp @@ -0,0 +1,26 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include +#include + +void OvCore::ECS::Components::UI::UIInspectorUtils::DrawCanvasRequirement( + OvUI::Internal::WidgetContainer& p_root, + const ECS::Actor& p_owner +) +{ + if (OvCore::ECS::Components::UI::UITransformResolver::HasActiveUIData(p_owner)) + { + return; + } + + // The inspector lays components out in a two column grid, so both cells of the row must be filled + p_root.CreateWidget("Canvas", OVUI_STYLE(Warning)); + p_root.CreateWidget("Required on a parent actor to render this component"); +} 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..edae88c54 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/UI/UITransformResolver.cpp @@ -0,0 +1,216 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include + +#include +#include +#include +#include + +namespace +{ + 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) +{ + return !IsHorizontalStretch(p_anchorPreset); +} + +bool OvCore::ECS::Components::UI::UITransformResolver::IsVerticalPositionEditable(OvCore::ECS::Components::CTransform::EUIAnchorPreset p_anchorPreset) +{ + return !IsVerticalStretch(p_anchorPreset); +} + +bool OvCore::ECS::Components::UI::UITransformResolver::IsHorizontalStretch(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 true; + default: + return false; + } +} + +bool OvCore::ECS::Components::UI::UITransformResolver::IsVerticalStretch(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 true; + default: + return false; + } +} + +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(); +} + +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(); + const auto fallbackWidth = std::isfinite(p_elementSize.x) ? std::max(p_elementSize.x, 0.0f) : 0.0f; + const auto fallbackHeight = std::isfinite(p_elementSize.y) ? std::max(p_elementSize.y, 0.0f) : 0.0f; + return { + std::isfinite(size.x) && size.x > 0.0f ? size.x : fallbackWidth, + std::isfinite(size.y) && size.y > 0.0f ? size.y : fallbackHeight + }; +} diff --git a/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp b/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp index cd4840c0c..5e83eb746 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; @@ -30,6 +31,7 @@ namespace case EFileType::SHADER: return "Pick Shader"; case EFileType::MATERIAL: return "Pick Material"; case EFileType::SOUND: return "Pick Sound"; + case EFileType::FONT: return "Pick Font"; case EFileType::SCRIPT: return "Pick Script"; case EFileType::SCENE: return "Pick Scene"; case EFileType::PREFAB: return "Pick Prefab"; @@ -105,6 +107,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 0d815d8e3..3e02b85c7 100644 --- a/Sources/OvCore/src/OvCore/Rendering/EngineBufferRenderFeature.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/EngineBufferRenderFeature.cpp @@ -34,21 +34,14 @@ OvCore::Rendering::EngineBufferRenderFeature::EngineBufferRenderFeature( void OvCore::Rendering::EngineBufferRenderFeature::SetCamera(const OvRendering::Entities::Camera& p_camera) { - struct - { - OvMaths::FMatrix4 viewMatrix; - OvMaths::FMatrix4 projectionMatrix; - OvMaths::FVector3 cameraPosition; - } uboDataPage{ + m_frameCameraData = { .viewMatrix = OvMaths::FMatrix4::Transpose(p_camera.GetViewMatrix()), .projectionMatrix = OvMaths::FMatrix4::Transpose(p_camera.GetProjectionMatrix()), - .cameraPosition = p_camera.GetPosition() + .position = p_camera.GetPosition() }; - m_engineBuffer->Upload(&uboDataPage, baregl::data::BufferMemoryRange{ - .offset = sizeof(OvMaths::FMatrix4), // Skip uploading the first matrix (Model matrix) - .size = sizeof(uboDataPage) - }); + UploadCameraData(m_frameCameraData); + m_cameraOverrideActive = false; } void OvCore::Rendering::EngineBufferRenderFeature::OnBeginFrame(const OvRendering::Data::FrameDescriptor& p_frameDescriptor) @@ -58,29 +51,41 @@ void OvCore::Rendering::EngineBufferRenderFeature::OnBeginFrame(const OvRenderin auto currentTime = std::chrono::high_resolution_clock::now(); auto elapsedTime = std::chrono::duration_cast>(currentTime - m_startTime); - struct + m_frameCameraData = { + .viewMatrix = OvMaths::FMatrix4::Transpose(p_frameDescriptor.camera->GetViewMatrix()), + .projectionMatrix = OvMaths::FMatrix4::Transpose(p_frameDescriptor.camera->GetProjectionMatrix()), + .position = p_frameDescriptor.camera->GetPosition() + }; + + struct FrameCameraData { OvMaths::FMatrix4 viewMatrix; OvMaths::FMatrix4 projectionMatrix; - OvMaths::FVector3 cameraPosition; + OvMaths::FVector3 position; float elapsedTime; } uboDataPage{ - .viewMatrix = OvMaths::FMatrix4::Transpose(p_frameDescriptor.camera->GetViewMatrix()), - .projectionMatrix = OvMaths::FMatrix4::Transpose(p_frameDescriptor.camera->GetProjectionMatrix()), - .cameraPosition = p_frameDescriptor.camera->GetPosition(), + .viewMatrix = m_frameCameraData.viewMatrix, + .projectionMatrix = m_frameCameraData.projectionMatrix, + .position = m_frameCameraData.position, .elapsedTime = elapsedTime.count() }; + static_assert( + sizeof(FrameCameraData) == sizeof(CameraData) + sizeof(float), + "FrameCameraData must match the engine UBO frame page" + ); m_engineBuffer->Upload(&uboDataPage, baregl::data::BufferMemoryRange{ .offset = sizeof(OvMaths::FMatrix4), // Skip uploading the first matrix (Model matrix) .size = sizeof(uboDataPage) }); + m_cameraOverrideActive = false; m_engineBuffer->Bind(baregl::types::EBufferType::UNIFORM, 0); } void OvCore::Rendering::EngineBufferRenderFeature::OnEndFrame() { + m_cameraOverrideActive = false; m_engineBuffer->Unbind(); } @@ -90,20 +95,62 @@ void OvCore::Rendering::EngineBufferRenderFeature::OnBeforeDraw(OvRendering::Dat OvTools::Utils::OptRef descriptor; - if (p_drawable.TryGetDescriptor(descriptor)) + if (!p_drawable.TryGetDescriptor(descriptor)) { - const auto modelMatrix = OvMaths::FMatrix4::Transpose(descriptor->modelMatrix); - - // Upload model matrix (First matrix in the UBO) - m_engineBuffer->Upload(&modelMatrix, baregl::data::BufferMemoryRange{ - .offset = 0, - .size = sizeof(modelMatrix) - }); + RestoreFrameCamera(); + return; + } + + const auto modelMatrix = OvMaths::FMatrix4::Transpose(descriptor->modelMatrix); - // Upload user matrix (Last matrix in the UBO) - m_engineBuffer->Upload(&descriptor->userMatrix, baregl::data::BufferMemoryRange{ - .offset = kUBOSize - sizeof(modelMatrix), - .size = sizeof(modelMatrix) + // Upload model matrix (First matrix in the UBO) + m_engineBuffer->Upload(&modelMatrix, baregl::data::BufferMemoryRange{ + .offset = 0, + .size = sizeof(modelMatrix) + }); + + // Upload user matrix (Last matrix in the UBO) + m_engineBuffer->Upload(&descriptor->userMatrix, baregl::data::BufferMemoryRange{ + .offset = kUBOSize - sizeof(modelMatrix), + .size = sizeof(modelMatrix) + }); + + if (descriptor->viewMatrixOverride || descriptor->projectionMatrixOverride) + { + UploadCameraData({ + .viewMatrix = descriptor->viewMatrixOverride ? + OvMaths::FMatrix4::Transpose(*descriptor->viewMatrixOverride) : + m_frameCameraData.viewMatrix, + .projectionMatrix = descriptor->projectionMatrixOverride ? + OvMaths::FMatrix4::Transpose(*descriptor->projectionMatrixOverride) : + m_frameCameraData.projectionMatrix, + .position = descriptor->viewMatrixOverride ? + OvMaths::FVector3::Zero : + m_frameCameraData.position }); + m_cameraOverrideActive = true; } + else + { + RestoreFrameCamera(); + } +} + +void OvCore::Rendering::EngineBufferRenderFeature::UploadCameraData(const CameraData& p_cameraData) +{ + m_engineBuffer->Upload(&p_cameraData, baregl::data::BufferMemoryRange{ + .offset = sizeof(OvMaths::FMatrix4), // Skip uploading the model matrix. + .size = sizeof(p_cameraData) + }); +} + +void OvCore::Rendering::EngineBufferRenderFeature::RestoreFrameCamera() +{ + if (!m_cameraOverrideActive) + { + return; + } + + UploadCameraData(m_frameCameraData); + m_cameraOverrideActive = false; } diff --git a/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp b/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp index 9983fa6ae..598c33fc2 100644 --- a/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp @@ -4,13 +4,19 @@ * @licence: MIT */ +#include +#include #include #include +#include #include #include #include #include +#include +#include +#include #include #include #include @@ -23,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -163,6 +170,239 @@ 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, + bool p_preserveAspect = false + ) + { + 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 + )) + { + if ( + p_preserveAspect && + p_elementSize.x > 0.0f && + p_elementSize.y > 0.0f && + resolvedElement.effectiveSize.x > 0.0f && + resolvedElement.effectiveSize.y > 0.0f + ) + { + const float fitScale = std::min( + resolvedElement.effectiveSize.x / p_elementSize.x, + resolvedElement.effectiveSize.y / p_elementSize.y + ); + descriptor.modelMatrix = resolvedElement.frameMatrix * OvMaths::FMatrix4::Scaling({ + fitScale, + fitScale, + 1.0f + }); + } + else + { + 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_image.GetPreserveAspect() + ) + ); + + 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 OvMaths::FVector2 textLayoutSize = hasResolvedElement ? + OvMaths::FVector2{ + resolvedElement.widthDriven ? resolvedElement.effectiveSize.x : 0.0f, + resolvedElement.heightDriven ? resolvedElement.effectiveSize.y : 0.0f + } : + owner.transform.GetUISize(); + + auto* mesh = p_text.GetMesh(textLayoutSize); + if (!mesh) return; + const auto renderedTextSize = p_text.GetSize(textLayoutSize); + + 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 +440,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 +456,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 +483,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 +521,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 +576,7 @@ SceneRenderer::SceneDrawablesDescriptor OvCore::Rendering::SceneRenderer::ParseS .visibilityFlags = materialRenderer->GetVisibilityFlags(), .bounds = bounds }); - + drawable.AddDescriptor({ transform.GetWorldMatrix(), materialRenderer->GetUserMatrix() @@ -331,6 +591,8 @@ SceneRenderer::SceneDrawablesDescriptor OvCore::Rendering::SceneRenderer::ParseS } } + AppendHierarchyUIDrawables(result, scene, uiFrameResolver); + return result; } @@ -368,7 +630,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 +696,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..7be5d54ab --- /dev/null +++ b/Sources/OvCore/src/OvCore/Rendering/UIRenderingUtils.cpp @@ -0,0 +1,703 @@ +/** +* @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; + constexpr float kUIWorldPreviewScale = 0.01f; + + 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::FVector2 ClampElementSize(const OvMaths::FVector2& p_size) + { + return { + std::max(KeepFinite(p_size.x, 0.0f), 0.0f), + std::max(KeepFinite(p_size.y, 0.0f), 0.0f) + }; + } + + 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, + const OvMaths::FVector2& p_effectiveSize, + bool p_drivenByLayout + ) + { + if (p_drivenByLayout) + { + return p_layoutOffset; + } + + const auto anchorPreset = p_transform.GetUIAnchorPreset(); + const auto& pivot = p_transform.GetUIPivot(); + const auto anchorRatio = OvCore::ECS::Components::UI::UITransformResolver::GetAnchorRatio(anchorPreset); + const OvMaths::FVector2 anchorOffset = { + OvCore::ECS::Components::UI::UITransformResolver::IsHorizontalStretch(anchorPreset) ? + pivot.x * p_effectiveSize.x * 0.5f : + KeepFinite(p_parentSize.x, 0.0f) * anchorRatio.x, + OvCore::ECS::Components::UI::UITransformResolver::IsVerticalStretch(anchorPreset) ? + -pivot.y * p_effectiveSize.y * 0.5f : + 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_effectiveSize, + 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; + } + + OvMaths::FVector3 TransformUIDirection( + const OvMaths::FMatrix4& p_matrix, + const OvMaths::FVector2& p_direction + ) + { + const auto result = p_matrix * OvMaths::FVector4{ p_direction.x, p_direction.y, 0.0f, 0.0f }; + return { result.x, result.y, result.z }; + } + + OvMaths::FQuaternion CreateUIRotation( + const OvMaths::FVector3& p_xAxis, + const OvMaths::FVector3& p_yAxis + ) + { + constexpr float kMinimumAxisLength = 0.0001f; + if ( + OvMaths::FVector3::Length(p_xAxis) < kMinimumAxisLength || + OvMaths::FVector3::Length(p_yAxis) < kMinimumAxisLength + ) + { + return OvMaths::FQuaternion::Identity; + } + + const auto xAxis = OvMaths::FVector3::Normalize(p_xAxis); + const auto yAxis = OvMaths::FVector3::Normalize(p_yAxis); + const auto forward = OvMaths::FVector3::Cross(xAxis, yAxis); + if (OvMaths::FVector3::Length(forward) < kMinimumAxisLength) + { + return OvMaths::FQuaternion::Identity; + } + + return OvMaths::FQuaternion::LookAt(OvMaths::FVector3::Normalize(forward), yAxis); + } +} + +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 = ClampElementSize(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 auto elementSize = ClampElementSize(p_elementSize); + const ElementKey key{ + .actor = &p_actor, + .width = elementSize.x, + .height = 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, 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(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; + 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; + } + + const auto layoutData = GetLayoutData(p_actor, parentSize); + + 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); + p_outElement.widthDriven = transform.GetUISize().x > 0.0f; + p_outElement.heightDriven = transform.GetUISize().y > 0.0f; + if (layoutData.hasDirectWidth) + { + p_outElement.effectiveSize.x = layoutData.directSize.x; + p_outElement.widthDriven = true; + } + if (layoutData.hasDirectHeight) + { + p_outElement.effectiveSize.y = layoutData.directSize.y; + p_outElement.heightDriven = true; + } + + if (!layoutData.drivenByLayout) + { + const auto anchorPreset = transform.GetUIAnchorPreset(); + if (OvCore::ECS::Components::UI::UITransformResolver::IsHorizontalStretch(anchorPreset)) + { + p_outElement.effectiveSize.x = std::max(parentSize.x, 0.0f); + p_outElement.widthDriven = true; + } + if (OvCore::ECS::Components::UI::UITransformResolver::IsVerticalStretch(anchorPreset)) + { + p_outElement.effectiveSize.y = std::max(parentSize.y, 0.0f); + p_outElement.heightDriven = true; + } + } + 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 + ); + const float rotation = transform.GetUIRotation() * kDegreesToRadians; + p_outElement.xPositionDirection = { std::cos(rotation), std::sin(rotation) }; + p_outElement.yPositionDirection = { -std::sin(rotation), std::cos(rotation) }; + p_outElement.xWorldAxis = TransformUIDirection(parentFrameMatrix, p_outElement.xPositionDirection); + p_outElement.yWorldAxis = TransformUIDirection(parentFrameMatrix, p_outElement.yPositionDirection); + 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 OvMaths::FVector2& p_parentSize +) const +{ + const ElementKey key{ + .actor = &p_actor, + .width = p_parentSize.x, + .height = p_parentSize.y + }; + + if (const auto it = m_layoutDataCache.find(key); 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, p_parentSize); 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(key, cachedLayoutData); + return cachedLayoutData; +} + +OvMaths::FVector2 OvCore::Rendering::UIRenderingUtils::ClampCanvasSize(const OvMaths::FVector2& p_canvasSize) +{ + return { + std::max(KeepFinite(p_canvasSize.x, 1.0f), 1.0f), + std::max(KeepFinite(p_canvasSize.y, 1.0f), 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::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(); +} + +float OvCore::Rendering::UIRenderingUtils::GetUIWorldScale(bool p_screenSpace) +{ + if (p_screenSpace) + { + return 1.0f; + } + + return kUIWorldPreviewScale; +} + +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::ResolveUIGizmoTransform( + const UIFrameResolver& p_frameResolver, + const OvCore::ECS::Actor& p_actor, + ResolvedUIGizmoTransform& p_outTransform +) +{ + if (p_actor.GetComponent()) + { + return false; + } + + ResolvedUIElement resolvedElement; + if (!p_frameResolver.ResolveElement( + p_actor, + resolvedElement + )) + { + return false; + } + + p_outTransform.position = TransformUIElementPivot(resolvedElement); + p_outTransform.xPositionDirection = resolvedElement.xPositionDirection; + p_outTransform.yPositionDirection = resolvedElement.yPositionDirection; + p_outTransform.xWorldAxis = resolvedElement.xWorldAxis; + p_outTransform.yWorldAxis = resolvedElement.yWorldAxis; + p_outTransform.rotation = CreateUIRotation(p_outTransform.xWorldAxis, p_outTransform.yWorldAxis); + p_outTransform.screenSpace = resolvedElement.screenSpace; + return true; +} 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/SceneSystem/Scene.cpp b/Sources/OvCore/src/OvCore/SceneSystem/Scene.cpp index fe219e1f0..9a88809b9 100644 --- a/Sources/OvCore/src/OvCore/SceneSystem/Scene.cpp +++ b/Sources/OvCore/src/OvCore/SceneSystem/Scene.cpp @@ -5,8 +5,11 @@ */ #include +#include +#include #include #include +#include #include #include @@ -466,6 +469,18 @@ std::vector& OvCore::SceneSystem::Scene::GetActors() return m_actors; } +bool OvCore::SceneSystem::Scene::MoveActorToEnd(ECS::Actor& p_actor) +{ + const auto it = std::find(m_actors.begin(), m_actors.end(), &p_actor); + if (it == m_actors.end()) + { + return false; + } + + std::rotate(it, std::next(it), m_actors.end()); + return true; +} + const OvCore::SceneSystem::Scene::FastAccessComponents& OvCore::SceneSystem::Scene::GetFastAccessComponents() const { return m_fastAccessComponents; @@ -479,9 +494,38 @@ void OvCore::SceneSystem::Scene::OnSerialize(tinyxml2::XMLDocument & p_doc, tiny tinyxml2::XMLNode* actorsNode = p_doc.NewElement("actors"); sceneNode->InsertEndChild(actorsNode); - for (auto& actor : m_actors) + std::unordered_set serializedActors; + const std::function serializeHierarchy = [&](ECS::Actor& p_actor) { - actor->OnSerialize(p_doc, actorsNode); + if (!serializedActors.emplace(&p_actor).second) + { + return; + } + + p_actor.OnSerialize(p_doc, actorsNode); + for (auto* child : p_actor.GetChildren()) + { + if (child) + { + serializeHierarchy(*child); + } + } + }; + + for (auto* actor : m_actors) + { + if (actor && !actor->HasParent()) + { + serializeHierarchy(*actor); + } + } + + for (auto* actor : m_actors) + { + if (actor) + { + serializeHierarchy(*actor); + } } } 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..6189f2076 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, + "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, + "GetPreserveAspect", &UI::CImage::GetPreserveAspect, + "SetPreserveAspect", &UI::CImage::SetPreserveAspect + ); + + 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 2fe366bf0..984154e96 100644 --- a/Sources/OvEditor/include/OvEditor/Core/Context.h +++ b/Sources/OvEditor/include/OvEditor/Core/Context.h @@ -13,11 +13,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -94,7 +96,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..a83dc095f 100644 --- a/Sources/OvEditor/include/OvEditor/Core/GizmoBehaviour.h +++ b/Sources/OvEditor/include/OvEditor/Core/GizmoBehaviour.h @@ -24,6 +24,16 @@ namespace OvEditor::Core class GizmoBehaviour { public: + struct UITranslationContext + { + OvMaths::FVector3 origin = OvMaths::FVector3::Zero; + OvMaths::FVector2 xPositionDirection = { 1.0f, 0.0f }; + OvMaths::FVector2 yPositionDirection = { 0.0f, 1.0f }; + OvMaths::FVector3 xWorldAxis = OvMaths::FVector3::Right; + OvMaths::FVector3 yWorldAxis = OvMaths::FVector3::Up; + bool screenSpace = false; + }; + enum class EDirection { X, @@ -42,8 +52,15 @@ namespace OvEditor::Core * @param p_cameraPosition * @param p_operation * @param p_direction + * @param p_uiTranslationContext */ - 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 UITranslationContext* p_uiTranslationContext = nullptr + ); /** * Stops the gizmo picking behaviour @@ -143,8 +160,27 @@ namespace OvEditor::Core EDirection m_direction; OvMaths::FTransform m_originalTransform; OvMaths::FVector3 m_initialOffset; + OvMaths::FVector2 m_originalUIPosition = OvMaths::FVector2::Zero; + OvMaths::FVector2 m_uiPositionDirection = OvMaths::FVector2::Zero; + OvMaths::FVector3 m_uiWorldAxis = OvMaths::FVector3::Zero; + bool m_isUITranslation = false; + bool m_isUIScreenSpace = false; OvMaths::FVector2 m_originMouse; OvMaths::FVector2 m_currentMouse; OvMaths::FVector2 m_screenDirection; }; + + /* Gizmo axis bitmask, using GizmoBehaviour::EDirection values as bit indices */ + constexpr int kGizmoAxisX = 1 << 0; + constexpr int kGizmoAxisY = 1 << 1; + constexpr int kGizmoAxisZ = 1 << 2; + constexpr int kGizmoAxisAll = kGizmoAxisX | kGizmoAxisY | kGizmoAxisZ; + + /** + * Returns the gizmo axes that the given operation can drive on the given user interface actor + * @param p_actor + * @param p_operation + * @param p_screenSpace + */ + int GetUIGizmoAxes(const OvCore::ECS::Actor& p_actor, EGizmoOperation p_operation, bool p_screenSpace); } 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..0c1eb7d4a 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 @@ -18,6 +21,7 @@ #include #include "OvEditor/Core/Context.h" +#include "OvEditor/Core/GizmoBehaviour.h" namespace OvEditor::Rendering { @@ -44,17 +48,25 @@ namespace OvEditor::Rendering * @param p_operation * @param p_pickable (Determine the shader to use to render the gizmo) * @param p_highlightedDirection + * @param p_viewMatrixOverride + * @param p_projectionMatrixOverride + * @param p_scaleOverride + * @param p_visibleAxes (Gizmo axis bitmask, see kGizmoAxis* constants) */ void DrawGizmo( const OvMaths::FVector3& p_position, 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, + int p_visibleAxes = OvEditor::Core::kGizmoAxisAll ); 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 262c71c3d..56f95bc29 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, + int p_visibleAxes = OvEditor::Core::kGizmoAxisAll ); private: diff --git a/Sources/OvEditor/premake5.lua b/Sources/OvEditor/premake5.lua index e452086ce..94dcc6889 100644 --- a/Sources/OvEditor/premake5.lua +++ b/Sources/OvEditor/premake5.lua @@ -62,7 +62,7 @@ project "OvEditor" -- Dependencies that others depend on - must come after "assimp", "glfw", - } + } filter { "configurations:Debug" } defines { "DEBUG", "_DEBUG" } diff --git a/Sources/OvEditor/src/OvEditor/Core/CameraController.cpp b/Sources/OvEditor/src/OvEditor/Core/CameraController.cpp index 036c685be..2f5e38478 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,138 @@ 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 + ( + 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 + ( + 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 ( - 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 + ( + 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 ps = p_actor.GetComponent()) + return distance; + } + + bool TryGetUIFocusTarget( + OvCore::ECS::Actor& p_actor, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver, + ActorFocusTarget& p_outTarget + ) + { + // In screen space the resolved matrices are expressed in canvas units, so they cannot be + // used as a world position to move the camera to + if (p_uiFrameResolver.IsScreenSpace()) { - 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); + return false; } - if (auto pc = p_actor.GetComponent()) + 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 = 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 +179,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 +276,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 +298,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 +363,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 +379,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 3ae8069ff..4c1790528 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; @@ -167,7 +173,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); @@ -183,6 +191,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..3f8df251b 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(); } ); @@ -137,7 +146,7 @@ void OvEditor::Core::Editor::SetupUI() } else { - // SOUND, FONT, UNKNOWN → open with OS default + // SOUND, FONT, UNKNOWN use the OS default application. OvTools::Utils::SystemCalls::OpenFile(EDITOR_EXEC(GetRealPath(path))); } } diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 27d38924a..c7bde1eff 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 + 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()) @@ -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..b038e6dd5 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,57 @@ 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 UITranslationContext* p_uiTranslationContext +) { 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_uiTranslationContext) + { + m_originalTransform.SetWorldPosition(p_uiTranslationContext->origin); + } + 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_uiTranslationContext != nullptr; + m_isUIScreenSpace = m_isUITranslation && p_uiTranslationContext->screenSpace; + m_uiPositionDirection = OvMaths::FVector2::Zero; + m_uiWorldAxis = OvMaths::FVector3::Zero; + + if (m_isUITranslation) + { + m_originalUIPosition = p_target.transform.GetUIPosition(); + if (m_direction == EDirection::X) + { + m_uiPositionDirection = p_uiTranslationContext->xPositionDirection; + m_uiWorldAxis = p_uiTranslationContext->xWorldAxis; + } + else if (m_direction == EDirection::Y) + { + m_uiPositionDirection = p_uiTranslationContext->yPositionDirection; + m_uiWorldAxis = p_uiTranslationContext->yWorldAxis; + } + } + else + { + m_originalUIPosition = OvMaths::FVector2::Zero; + } } void OvEditor::Core::GizmoBehaviour::StopPicking() { m_target = nullptr; + m_isUITranslation = false; + m_isUIScreenSpace = false; + m_uiPositionDirection = OvMaths::FVector2::Zero; + m_uiWorldAxis = OvMaths::FVector3::Zero; } OvMaths::FVector3 OvEditor::Core::GizmoBehaviour::GetFakeDirection() const @@ -122,6 +160,84 @@ 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 float unitsScale = OvMaths::FVector3::Length(m_uiWorldAxis); + if (unitsScale <= 0.0001f) + { + return; + } + + const auto mouseDelta = m_currentMouse - m_originMouse; + const auto screenAxis = OvMaths::FVector2::Normalize({ m_uiWorldAxis.x, -m_uiWorldAxis.y }); + auto translationUnits = OvMaths::FVector2::Dot(mouseDelta, screenAxis) / unitsScale; + + if (IsSnappedBehaviourEnabled()) + { + translationUnits = SnapValue(translationUnits, OvEditor::Settings::EditorSettings::TranslationSnapUnit); + } + + m_target->transform.SetUIPosition(m_originalUIPosition + m_uiPositionDirection * translationUnits); + return; + } + + const auto ray = GetMouseRay(m_currentMouse, p_viewMatrix, p_projectionMatrix, p_viewSize); + const float axisScale = OvMaths::FVector3::Length(m_uiWorldAxis); + if (axisScale <= 0.0001f) + { + return; + } + const OvMaths::FVector3 direction = OvMaths::FVector3::Normalize(m_uiWorldAxis); + 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 translationUnits = OvMaths::FVector3::Dot(translationVector, direction) / axisScale; + + if (IsSnappedBehaviourEnabled()) + { + translationUnits = SnapValue(translationUnits, OvEditor::Settings::EditorSettings::TranslationSnapUnit); + } + + m_target->transform.SetUIPosition(m_originalUIPosition + m_uiPositionDirection * translationUnits); + 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); @@ -316,3 +432,26 @@ OvMaths::FVector3 OvEditor::Core::GizmoBehaviour::GetMouseRay(const OvMaths::FVe return OvMaths::FVector3(farthestPoint.x, farthestPoint.y, farthestPoint.z) * nearestPoint.w - OvMaths::FVector3(nearestPoint.x, nearestPoint.y, nearestPoint.z) * farthestPoint.w; ; } + +int OvEditor::Core::GetUIGizmoAxes(const OvCore::ECS::Actor& p_actor, EGizmoOperation p_operation, bool p_screenSpace) +{ + // User interface elements are laid out on the canvas plane, so the Z axis is meaningless in screen space + int axes = p_screenSpace ? kGizmoAxisX | kGizmoAxisY : kGizmoAxisAll; + + if (p_operation != EGizmoOperation::TRANSLATE) + { + return axes; + } + + if (!p_actor.transform.IsHorizontalUIPositionEditable()) + { + axes &= ~kGizmoAxisX; + } + + if (!p_actor.transform.IsVerticalUIPositionEditable()) + { + axes &= ~kGizmoAxisY; + } + + return axes; +} 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/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index 51ee07857..3efd6c2e4 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -396,6 +396,10 @@ OvEditor::Panels::Hierarchy::Hierarchy ConsiderWidget(*p_element.second); p_element.first->DetachFromParent(); + if (auto* currentScene = EDITOR_CONTEXT(sceneManager).GetCurrentScene()) + { + currentScene->MoveActorToEnd(*p_element.first); + } }; AddPlugin(nullptr, nullptr); diff --git a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp index 09273b619..755e07357 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp @@ -4,10 +4,14 @@ * @licence: MIT */ +#include #include #include +#include #include #include +#include +#include #include #include @@ -26,6 +30,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -75,6 +85,10 @@ namespace { return !p_actor.GetComponent(); } + else if constexpr (std::is_base_of_v) + { + return !p_actor.GetComponent(); + } else { return !p_actor.GetComponent(); @@ -105,6 +119,11 @@ namespace CreateComponentInfo("Audio Listener"), CreateComponentInfo("Post Process Stack"), CreateComponentInfo("Reflection Probe"), + CreateComponentInfo("Canvas"), + CreateComponentInfo("Horizontal Layout"), + CreateComponentInfo("Image"), + CreateComponentInfo("Text"), + CreateComponentInfo("Vertical Layout"), }); } @@ -123,11 +142,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 +171,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 +199,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 +404,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 +442,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 +485,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..8d7edef2e 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 @@ -31,6 +35,31 @@ namespace return std::nullopt; } + + std::optional ResolveUIGizmoContext( + OvCore::ECS::Actor& p_actor, + const OvCore::Rendering::UIRenderingUtils::UIFrameResolver& p_uiFrameResolver + ) + { + OvCore::Rendering::UIRenderingUtils::ResolvedUIGizmoTransform resolvedTransform; + if (!OvCore::Rendering::UIRenderingUtils::ResolveUIGizmoTransform( + p_uiFrameResolver, + p_actor, + resolvedTransform + )) + { + return std::nullopt; + } + + return OvEditor::Core::GizmoBehaviour::UITranslationContext{ + .origin = resolvedTransform.position, + .xPositionDirection = resolvedTransform.xPositionDirection, + .yPositionDirection = resolvedTransform.yPositionDirection, + .xWorldAxis = resolvedTransform.xWorldAxis, + .yWorldAxis = resolvedTransform.yWorldAxis, + .screenSpace = resolvedTransform.screenSpace + }; + } } OvEditor::Panels::SceneView::SceneView @@ -104,6 +133,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 +183,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 +259,27 @@ 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 + ); m_gizmoOperations.StartPicking( - EDITOR_EXEC(GetSelectedActor()), + selectedActor, m_camera.GetPosition(), m_currentOperation, - m_highlightedGizmoDirection.value()); + m_highlightedGizmoDirection.value(), + uiGizmoContext ? &uiGizmoContext.value() : nullptr + ); } else if (m_highlightedActor) { diff --git a/Sources/OvEditor/src/OvEditor/Panels/Toolbar.cpp b/Sources/OvEditor/src/OvEditor/Panels/Toolbar.cpp index 16050c417..495bfc294 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Toolbar.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Toolbar.cpp @@ -92,6 +92,19 @@ OvEditor::Panels::Toolbar::Toolbar m_nextButton->ClickedEvent += EDITOR_BIND(NextFrame); refreshButton.ClickedEvent += EDITOR_BIND(RefreshScripts); + CreateWidget(0).lineBreak = false; + m_sceneUIButton = &CreateWidget(editorResources->GetTexture("Font")->GetTexture().GetID(), iconSize); + m_sceneUIButton->lineBreak = false; + m_sceneUIButton->tooltip = "Toggle Scene View UI screen-space mode"; + m_sceneUIButton->ClickedEvent += []() { EDITOR_EXEC(ToggleSceneUIRendering()); }; + + auto updateSceneUIRendering = [this](bool p_enabled) { + m_sceneUIButton->tint = GetButtonTint(p_enabled); + }; + + updateSceneUIRendering(EDITOR_EXEC(IsSceneUIRenderingEnabled())); + EDITOR_EVENT(SceneUIRenderingChangedEvent) += updateSceneUIRendering; + EDITOR_EVENT(EditorModeChangedEvent) += [this](Core::EditorActions::EEditorMode p_mode) { using enum Core::EditorActions::EEditorMode; m_playButton->disabled = !(p_mode == EDIT || p_mode == FRAME_BY_FRAME || p_mode == PAUSE); diff --git a/Sources/OvEditor/src/OvEditor/Rendering/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 35e01ce58..a3a8f8710 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,55 @@ namespace { auto lightBuffer = std::make_unique(); - const auto lightMatrices = std::to_array({ + const std::array lightMatrices = { CreateDebugDirectionalLight(), CreateDebugAmbientLight() - }); + }; lightBuffer->Allocate(sizeof(lightMatrices), baregl::types::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::ResolvedUIGizmoTransform resolvedTransform; + if (!OvCore::Rendering::UIRenderingUtils::ResolveUIGizmoTransform( + p_uiFrameResolver, + p_actor, + resolvedTransform + )) + { + return false; + } + + p_position = resolvedTransform.position; + p_rotation = resolvedTransform.rotation; + return true; + } + + bool ShouldRenderWorldDebugElements(const OvRendering::Core::CompositeRenderer& p_renderer) + { + if (!p_renderer.HasDescriptor()) + { + return true; + } + + const auto& sceneDescriptor = p_renderer.GetDescriptor(); + return !sceneDescriptor.renderUIInScreenSpace; + } } class DebugCamerasRenderPass : public OvRendering::Core::ARenderPass @@ -132,6 +185,11 @@ class DebugCamerasRenderPass : public OvRendering::Core::ARenderPass ZoneScoped; TracyGpuZone("DebugCamerasRenderPass"); + if (!ShouldRenderWorldDebugElements(m_renderer)) + { + return; + } + using namespace OvRendering::Features; const auto lightingRenderFeature = OvTools::Utils::OptRef{ @@ -198,6 +256,11 @@ class DebugReflectionProbesRenderPass : public OvRendering::Core::ARenderPass ZoneScoped; TracyGpuZone("DebugReflectionProbesRenderPass"); + if (!ShouldRenderWorldDebugElements(m_renderer)) + { + return; + } + using namespace OvRendering::Features; const auto lightingRenderFeature = OvTools::Utils::OptRef{ @@ -272,6 +335,11 @@ class DebugLightsRenderPass : public OvRendering::Core::ARenderPass ZoneScoped; TracyGpuZone("DebugLightsRenderPass"); + if (!ShouldRenderWorldDebugElements(m_renderer)) + { + return; + } + auto& sceneDescriptor = m_renderer.GetDescriptor(); m_lightMaterial.SetProperty("u_Scale", OvEditor::Settings::EditorSettings::LightBillboardScale * 0.1f); @@ -332,26 +400,75 @@ class DebugActorRenderPass : public OvRendering::Core::ARenderPass { auto& selectedActor = debugSceneDescriptor.selectedActor.value(); const bool isActorHovered = debugSceneDescriptor.highlightedActor && debugSceneDescriptor.highlightedActor->GetID() == selectedActor.GetID(); + const bool renderWorldDebugElements = ShouldRenderWorldDebugElements(m_renderer); DrawActorDebugElements(selectedActor); - m_renderer.GetFeature().DrawOutline( + 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, - isActorHovered ? - kHoveredOutlineColor : - kSelectedOutlineColor, - kSelectedOutlineWidth - ); - m_renderer.Clear(false, true, false, OvMaths::FVector3::Zero); - m_renderer.GetFeature().DrawGizmo( - selectedActor.transform.GetWorldPosition(), - selectedActor.transform.GetWorldRotation(), - debugSceneDescriptor.gizmoOperation, - false, - debugSceneDescriptor.highlightedGizmoDirection + gizmoPosition, + gizmoRotation ); + std::optional gizmoViewMatrixOverride; + std::optional gizmoProjectionMatrixOverride; + std::optional gizmoScaleOverride; + int gizmoVisibleAxes = OvEditor::Core::kGizmoAxisAll; + if (hasUIGizmoTransform) + { + gizmoVisibleAxes = OvEditor::Core::GetUIGizmoAxes( + selectedActor, + debugSceneDescriptor.gizmoOperation, + uiFrameResolver.IsScreenSpace() + ); + + if (uiFrameResolver.IsScreenSpace()) + { + gizmoViewMatrixOverride = OvMaths::FMatrix4::Identity; + gizmoProjectionMatrixOverride = uiFrameResolver.CreateProjectionMatrix( + -kUIScreenSpaceGizmoDepth, + kUIScreenSpaceGizmoDepth + ); + gizmoScaleOverride = kUIScreenSpaceGizmoScale; + } + } + + if (renderWorldDebugElements || hasUIGizmoTransform) + { + m_renderer.GetFeature().DrawOutline( + selectedActor, + isActorHovered ? + kHoveredOutlineColor : + kSelectedOutlineColor, + kSelectedOutlineWidth + ); + m_renderer.Clear(false, true, false, OvMaths::FVector3::Zero); + m_renderer.GetFeature().DrawGizmo( + gizmoPosition, + gizmoRotation, + debugSceneDescriptor.gizmoOperation, + false, + debugSceneDescriptor.highlightedGizmoDirection, + gizmoViewMatrixOverride, + gizmoProjectionMatrixOverride, + gizmoScaleOverride, + gizmoVisibleAxes + ); + } } - if (debugSceneDescriptor.highlightedActor) + if (debugSceneDescriptor.highlightedActor && ShouldRenderWorldDebugElements(m_renderer)) { auto& highlightedActor = debugSceneDescriptor.highlightedActor.value(); @@ -367,55 +484,80 @@ class DebugActorRenderPass : public OvRendering::Core::ARenderPass { if (p_actor.IsActive()) { - /* Render static mesh outline and bounding spheres */ - if (OvEditor::Settings::EditorSettings::ShowGeometryBounds) + const bool renderWorldDebugElements = ShouldRenderWorldDebugElements(m_renderer); + + if (auto layout = p_actor.GetComponent()) { - auto modelRenderer = p_actor.GetComponent(); + DrawUIBounds(p_actor, layout->GetComputedSize()); + } - if (modelRenderer && modelRenderer->GetModel()) + if (renderWorldDebugElements) + { + /* Render static mesh outline and bounding spheres */ + if (OvEditor::Settings::EditorSettings::ShowGeometryBounds) { - DrawBoundingSpheres(*modelRenderer); + auto modelRenderer = p_actor.GetComponent(); + + if (modelRenderer && modelRenderer->GetModel()) + { + DrawBoundingSpheres(*modelRenderer); + } } - } - /* Render camera component frustum */ - if (auto cameraComponent = p_actor.GetComponent(); cameraComponent) - { - DrawCameraFrustum(*cameraComponent); - } + /* Render camera component frustum */ + if (auto cameraComponent = p_actor.GetComponent(); cameraComponent) + { + DrawCameraFrustum(*cameraComponent); + } - /* Render camera component frustum */ - if (auto reflectionProbeComponent = p_actor.GetComponent(); reflectionProbeComponent) - { - if (reflectionProbeComponent->GetInfluencePolicy() == OvCore::ECS::Components::CReflectionProbe::EInfluencePolicy::LOCAL) + /* Render reflection probe influence volume */ + if (auto reflectionProbeComponent = p_actor.GetComponent(); reflectionProbeComponent) { - DrawReflectionProbeInfluenceVolume(*reflectionProbeComponent); + if (reflectionProbeComponent->GetInfluencePolicy() == OvCore::ECS::Components::CReflectionProbe::EInfluencePolicy::LOCAL) + { + DrawReflectionProbeInfluenceVolume(*reflectionProbeComponent); + } } - } - /* Render the actor collider */ - if (p_actor.GetComponent()) - { - DrawActorCollider(p_actor); + /* Render the actor collider */ + if (p_actor.GetComponent()) + { + DrawActorCollider(p_actor); + } + + /* Render the actor ambient light */ + if (auto ambientBoxComp = p_actor.GetComponent()) + { + DrawAmbientBoxVolume(*ambientBoxComp); + } + + if (auto ambientSphereComp = p_actor.GetComponent()) + { + DrawAmbientSphereVolume(*ambientSphereComp); + } + + if (OvEditor::Settings::EditorSettings::ShowLightBounds) + { + if (auto light = p_actor.GetComponent()) + { + DrawLightBounds(*light); + } + } } - /* Render the actor ambient light */ - if (auto ambientBoxComp = p_actor.GetComponent()) + if (auto image = p_actor.GetComponent()) { - DrawAmbientBoxVolume(*ambientBoxComp); + DrawUIBounds(p_actor, image->GetSize()); } - if (auto ambientSphereComp = p_actor.GetComponent()) + if (auto text = p_actor.GetComponent()) { - DrawAmbientSphereVolume(*ambientSphereComp); + DrawUIBounds(p_actor, text->GetSize()); } - if (OvEditor::Settings::EditorSettings::ShowLightBounds) + if (p_actor.GetComponent()) { - if (auto light = p_actor.GetComponent()) - { - DrawLightBounds(*light); - } + DrawCanvasBounds(p_actor); } for (auto& child : p_actor.GetChildren()) @@ -425,6 +567,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..df032ee26 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* kVisibleAxesUniform = "u_VisibleAxes"; +} + 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(kVisibleAxesUniform, OvEditor::Core::kGizmoAxisAll); /* 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(kVisibleAxesUniform, OvEditor::Core::kGizmoAxisAll); } 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, + int p_visibleAxes ) { 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(kVisibleAxesUniform, p_visibleAxes); + m_gizmoArrowMaterial.TrySetProperty(kVisibleAxesUniform, p_visibleAxes); 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 f46e3c38e..26425fbdf 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 cc4a3c098..c4cece546 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* kVisibleAxesUniform = "u_VisibleAxes"; void PreparePickingMaterial( const OvCore::ECS::Actor& p_actor, @@ -44,6 +50,45 @@ 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::ResolvedUIGizmoTransform resolvedTransform; + if (!OvCore::Rendering::UIRenderingUtils::ResolveUIGizmoTransform( + p_uiFrameResolver, + p_actor, + resolvedTransform + )) + { + return false; + } + + p_position = resolvedTransform.position; + p_rotation = resolvedTransform.rotation; + return true; + } + + bool ShouldPickWorldDebugElements(const OvRendering::Core::CompositeRenderer& p_renderer) + { + if (!p_renderer.HasDescriptor()) + { + return true; + } + + const auto& sceneDescriptor = p_renderer.GetDescriptor(); + return !sceneDescriptor.renderUIInScreenSpace; + } } OvEditor::Rendering::PickingRenderPass::PickingRenderPass(OvRendering::Core::CompositeRenderer& p_renderer) : @@ -63,6 +108,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(kVisibleAxesUniform, OvEditor::Core::kGizmoAxisAll); m_gizmoPickingMaterial.SetDepthTest(true); m_reflectionProbeMaterial.SetShader(EDITOR_CONTEXT(editorResources)->GetShader("PickingFallback")); @@ -121,6 +168,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); @@ -131,9 +179,13 @@ void OvEditor::Rendering::PickingRenderPass::Draw(OvRendering::Data::PipelineSta m_renderer.Clear(true, true, true); DrawPickableModels(pso, scene); - DrawPickableCameras(pso, scene); - DrawPickableReflectionProbes(pso, scene); - DrawPickableLights(pso, scene); + + if (ShouldPickWorldDebugElements(m_renderer)) + { + DrawPickableCameras(pso, scene); + DrawPickableReflectionProbes(pso, scene); + DrawPickableLights(pso, scene); + } // Clear depth, gizmos are rendered on top of everything else m_renderer.Clear(false, true, false); @@ -141,13 +193,52 @@ void OvEditor::Rendering::PickingRenderPass::Draw(OvRendering::Data::PipelineSta if (debugSceneDescriptor.selectedActor) { auto& selectedActor = debugSceneDescriptor.selectedActor.value(); - - DrawPickableGizmo( - pso, - selectedActor.transform.GetWorldPosition(), - selectedActor.transform.GetWorldRotation(), - debugSceneDescriptor.gizmoOperation + auto gizmoPosition = selectedActor.transform.GetWorldPosition(); + auto gizmoRotation = selectedActor.transform.GetWorldRotation(); + const bool pickWorldDebugElements = ShouldPickWorldDebugElements(m_renderer); + const bool hasUIGizmoTransform = TryGetUIActorGizmoTransform( + sceneDescriptor.includeUI, + uiFrameResolver, + selectedActor, + gizmoPosition, + gizmoRotation ); + std::optional gizmoViewMatrixOverride; + std::optional gizmoProjectionMatrixOverride; + std::optional gizmoScaleOverride; + int gizmoVisibleAxes = OvEditor::Core::kGizmoAxisAll; + if (hasUIGizmoTransform) + { + gizmoVisibleAxes = OvEditor::Core::GetUIGizmoAxes( + selectedActor, + debugSceneDescriptor.gizmoOperation, + uiFrameResolver.IsScreenSpace() + ); + + if (uiFrameResolver.IsScreenSpace()) + { + gizmoViewMatrixOverride = OvMaths::FMatrix4::Identity; + gizmoProjectionMatrixOverride = uiFrameResolver.CreateProjectionMatrix( + -kUIScreenSpaceGizmoDepth, + kUIScreenSpaceGizmoDepth + ); + gizmoScaleOverride = kUIScreenSpaceGizmoScale; + } + } + + if (pickWorldDebugElements || hasUIGizmoTransform) + { + DrawPickableGizmo( + pso, + gizmoPosition, + gizmoRotation, + debugSceneDescriptor.gizmoOperation, + gizmoViewMatrixOverride, + gizmoProjectionMatrixOverride, + gizmoScaleOverride, + gizmoVisibleAxes + ); + } } m_actorPickingFramebuffer.Unbind(); @@ -300,9 +391,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, + int p_visibleAxes ) { + m_gizmoPickingMaterial.TrySetProperty(kGizmoScaleUniform, p_scaleOverride.value_or(kDistanceBasedGizmoScale)); + m_gizmoPickingMaterial.TrySetProperty(kVisibleAxesUniform, p_visibleAxes); + auto modelMatrix = OvMaths::FMatrix4::Translation(p_position) * OvMaths::FQuaternion::ToMatrix4(OvMaths::FQuaternion::Normalize(p_rotation)); @@ -310,5 +408,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..3ca9ee048 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp @@ -19,6 +19,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include @@ -247,6 +252,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 +275,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 += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + ui.CreateWidget("Text").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + ui.CreateWidget("Horizontal Layout").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + ui.CreateWidget("Vertical Layout").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); others.CreateWidget("Camera").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); others.CreateWidget("Post Process Stack").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); others.CreateWidget("Reflection Probe").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); diff --git a/Sources/OvGame/include/OvGame/Core/Context.h b/Sources/OvGame/include/OvGame/Core/Context.h index d505a4aa1..b5d8b2ade 100644 --- a/Sources/OvGame/include/OvGame/Core/Context.h +++ b/Sources/OvGame/include/OvGame/Core/Context.h @@ -14,11 +14,13 @@ #include +#include #include #include #include #include #include +#include #include #include @@ -64,7 +66,9 @@ 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; }; diff --git a/Sources/OvGame/premake5.lua b/Sources/OvGame/premake5.lua index a4d4c68dc..a966f4cd9 100644 --- a/Sources/OvGame/premake5.lua +++ b/Sources/OvGame/premake5.lua @@ -58,7 +58,7 @@ project "OvGame" "OvTools", "OvUI", "OvWindowing" - } + } filter { "configurations:Debug" } defines { "DEBUG", "_DEBUG" } diff --git a/Sources/OvGame/src/OvGame/Core/Context.cpp b/Sources/OvGame/src/OvGame/Core/Context.cpp index 85fa0639c..56fd07cfd 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..e0489b961 --- /dev/null +++ b/Sources/OvRendering/include/OvRendering/Resources/Font.h @@ -0,0 +1,212 @@ +/** +* @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 + { + bool valid = false; + 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 = 0x20; + static constexpr uint32_t kBasicLatinGlyphCount = 0x7F - kFirstGlyph; + static constexpr uint32_t kFirstLatin1Glyph = 0xA0; + static constexpr uint32_t kLatin1GlyphCount = 0x100 - kFirstLatin1Glyph; + static constexpr uint32_t kGlyphCount = kBasicLatinGlyphCount + kLatin1GlyphCount; + + /** + * 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 ascender in atlas pixels + */ + float GetAscender() const; + + /** + * Returns the ascender in atlas pixels for the given requested pixel size + * @param p_pixelSize + */ + float GetAscender(float p_pixelSize) const; + + /** + * Returns the descender in atlas pixels + */ + float GetDescender() const; + + /** + * Returns the descender in atlas pixels for the given requested pixel size + * @param p_pixelSize + */ + float GetDescender(float p_pixelSize) const; + + /** + * Returns the glyph associated with the given Unicode code point + * @param p_codePoint + */ + const Glyph* GetGlyph(uint32_t p_codePoint) const; + + /** + * Returns the glyph associated with the given Unicode code point and requested pixel size + * @param p_codePoint + * @param p_pixelSize + */ + const Glyph* GetGlyph(uint32_t p_codePoint, 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; + float ascender = 25.6f; + float descender = -6.4f; + 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/include/OvRendering/Resources/Texture.h b/Sources/OvRendering/include/OvRendering/Resources/Texture.h index 37558b49c..fe4657b84 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Texture.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Texture.h @@ -30,6 +30,11 @@ namespace OvRendering::Resources */ baregl::Texture& GetTexture(); + /** + * Returns the resource content revision + */ + uint64_t GetRevision() const; + private: Texture(const std::string p_path, std::unique_ptr&& p_texture); ~Texture() = default; @@ -40,5 +45,6 @@ namespace OvRendering::Resources private: std::unique_ptr m_texture; + uint64_t m_revision = 0; }; } diff --git a/Sources/OvRendering/premake5.lua b/Sources/OvRendering/premake5.lua index 542226c05..d7be1d6d9 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 .. "baregl/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 0df749a39..8282a823c 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..5772c7e23 --- /dev/null +++ b/Sources/OvRendering/src/OvRendering/Resources/Font.cpp @@ -0,0 +1,708 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#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"; + constexpr size_t kInvalidGlyphIndex = OvRendering::Resources::Font::kGlyphCount; + + size_t ToGlyphIndex(uint32_t p_codePoint) + { + using Font = OvRendering::Resources::Font; + + if (p_codePoint >= Font::kFirstGlyph && p_codePoint < Font::kFirstGlyph + Font::kBasicLatinGlyphCount) + { + return p_codePoint - Font::kFirstGlyph; + } + + if (p_codePoint >= Font::kFirstLatin1Glyph && p_codePoint < Font::kFirstLatin1Glyph + Font::kLatin1GlyphCount) + { + return Font::kBasicLatinGlyphCount + p_codePoint - Font::kFirstLatin1Glyph; + } + + return kInvalidGlyphIndex; + } + + uint32_t ToGlyphCodePoint(size_t p_index) + { + using Font = OvRendering::Resources::Font; + return p_index < Font::kBasicLatinGlyphCount ? + Font::kFirstGlyph + static_cast(p_index) : + Font::kFirstLatin1Glyph + static_cast(p_index - Font::kBasicLatinGlyphCount); + } + + 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; + float ascender = kDefaultPixelSize * 0.8f; + float descender = -kDefaultPixelSize * 0.2f; + 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; + result.ascender = static_cast(face.handle->size->metrics.ascender) / 64.0f; + result.descender = static_cast(face.handle->size->metrics.descender) / 64.0f; + } + + for (uint32_t atlasSize = kMinimumAtlasSize; atlasSize <= kMaximumAtlasSize; atlasSize *= 2) + { + result.glyphs = {}; + std::vector alphaAtlas(static_cast(atlasSize) * atlasSize); + uint32_t cursorX = 0; + uint32_t cursorY = 0; + uint32_t rowHeight = 0; + bool fits = true; + + for (size_t i = 0; i < Font::kGlyphCount; ++i) + { + const uint32_t codePoint = ToGlyphCodePoint(i); + if (FT_Get_Char_Index(face.handle, codePoint) == 0) + { + continue; + } + + if (FT_Load_Char(face.handle, codePoint, FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL) != 0) + { + continue; + } + + 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.valid = true; + 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 (glyphWidth + kGlyphPadding > atlasSize) + { + fits = false; + break; + } + + 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)); +} + +float OvRendering::Resources::Font::GetAscender() const +{ + if (const auto* variant = GetActiveVariant(); variant) + { + return variant->ascender; + } + + return static_cast(m_activePixelSize) * 0.8f; +} + +float OvRendering::Resources::Font::GetAscender(float p_pixelSize) const +{ + if (const auto* variant = GetVariant(ToPixelSizeKey(p_pixelSize)); variant) + { + return variant->ascender; + } + + return static_cast(ToPixelSizeKey(p_pixelSize)) * 0.8f; +} + +float OvRendering::Resources::Font::GetDescender() const +{ + if (const auto* variant = GetActiveVariant(); variant) + { + return variant->descender; + } + + return -static_cast(m_activePixelSize) * 0.2f; +} + +float OvRendering::Resources::Font::GetDescender(float p_pixelSize) const +{ + const auto* variant = GetVariant(ToPixelSizeKey(p_pixelSize)); + return variant ? variant->descender : -static_cast(ToPixelSizeKey(p_pixelSize)) * 0.2f; +} + +const OvRendering::Resources::Font::Glyph* OvRendering::Resources::Font::GetGlyph(uint32_t p_codePoint) const +{ + const auto glyphIndex = ToGlyphIndex(p_codePoint); + const auto* variant = GetActiveVariant(); + if (glyphIndex == kInvalidGlyphIndex || !variant || !variant->glyphs[glyphIndex].valid) + { + return nullptr; + } + + return &variant->glyphs[glyphIndex]; +} + +const OvRendering::Resources::Font::Glyph* OvRendering::Resources::Font::GetGlyph(uint32_t p_codePoint, float p_pixelSize) const +{ + const auto glyphIndex = ToGlyphIndex(p_codePoint); + const auto* variant = GetVariant(ToPixelSizeKey(p_pixelSize)); + if (glyphIndex == kInvalidGlyphIndex || !variant || !variant->glyphs[glyphIndex].valid) + { + return nullptr; + } + + return &variant->glyphs[glyphIndex]; +} + +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 baregl::types; + + 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.ascender = bakedFont.ascender; + variant.descender = bakedFont.descender; + 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/OvRendering/src/OvRendering/Resources/Texture.cpp b/Sources/OvRendering/src/OvRendering/Resources/Texture.cpp index e9bb9673e..e21c8749d 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Texture.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Texture.cpp @@ -13,6 +13,11 @@ baregl::Texture& OvRendering::Resources::Texture::GetTexture() return *m_texture; } +uint64_t OvRendering::Resources::Texture::GetRevision() const +{ + return m_revision; +} + OvRendering::Resources::Texture::Texture(const std::string p_path, std::unique_ptr&& p_texture) : path(p_path) { SetTexture(std::move(p_texture)); @@ -22,4 +27,5 @@ void OvRendering::Resources::Texture::SetTexture(std::unique_ptr #include +#include +#include +#include namespace OvTools::Eventing { @@ -72,9 +75,24 @@ namespace OvTools::Eventing void Invoke(ArgTypes... p_args); private: - std::unordered_map m_callbacks; - ListenerID m_availableListenerID = 0; + /** + * Return true if the given listener has been removed while a dispatch is in progress + * @param p_listenerID + */ + bool IsPendingRemoval(ListenerID p_listenerID) const; + + /** + * Apply the listener additions and removals deferred during a dispatch + */ + void FlushPendingOperations(); + + private: + std::unordered_map m_callbacks; + std::vector> m_pendingAdditions; + std::vector m_pendingRemovals; + ListenerID m_availableListenerID = 0; + uint32_t m_invokeDepth = 0; }; } -#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..1ca8d6a6b 100644 --- a/Sources/OvTools/include/OvTools/Eventing/Event.inl +++ b/Sources/OvTools/include/OvTools/Eventing/Event.inl @@ -6,6 +6,8 @@ #pragma once +#include + #include "OvTools/Eventing/Event.h" namespace OvTools::Eventing @@ -14,20 +16,54 @@ namespace OvTools::Eventing ListenerID Event::AddListener(Callback p_callback) { ListenerID listenerID = m_availableListenerID++; - m_callbacks.emplace(listenerID, p_callback); + + if (m_invokeDepth > 0) + { + m_pendingAdditions.emplace_back(listenerID, std::move(p_callback)); + } + else + { + m_callbacks.emplace(listenerID, std::move(p_callback)); + } + return listenerID; } template ListenerID Event::operator+=(Callback p_callback) { - return AddListener(p_callback); + return AddListener(std::move(p_callback)); } template bool Event::RemoveListener(ListenerID p_listenerID) { - return m_callbacks.erase(p_listenerID) != 0; + if (m_invokeDepth == 0) + { + return m_callbacks.erase(p_listenerID) != 0; + } + + // Deferring the erase keeps a self-removing callback alive while it runs, and keeps the + // callback map stable while it is being iterated + if (m_callbacks.contains(p_listenerID) && !IsPendingRemoval(p_listenerID)) + { + m_pendingRemovals.push_back(p_listenerID); + return true; + } + + const auto pendingAddition = std::find_if( + m_pendingAdditions.begin(), + m_pendingAdditions.end(), + [p_listenerID](const auto& p_addition) { return p_addition.first == p_listenerID; } + ); + + if (pendingAddition != m_pendingAdditions.end()) + { + m_pendingAdditions.erase(pendingAddition); + return true; + } + + return false; } template @@ -39,19 +75,73 @@ namespace OvTools::Eventing template void Event::RemoveAllListeners() { - m_callbacks.clear(); + if (m_invokeDepth == 0) + { + m_callbacks.clear(); + return; + } + + m_pendingAdditions.clear(); + + for (const auto& listener : m_callbacks) + { + if (!IsPendingRemoval(listener.first)) + { + m_pendingRemovals.push_back(listener.first); + } + } } template uint64_t Event::GetListenerCount() { - return m_callbacks.size(); + return m_callbacks.size() + m_pendingAdditions.size() - m_pendingRemovals.size(); } template void Event::Invoke(ArgTypes... p_args) { - for (auto const& [key, value] : m_callbacks) - value(p_args...); + // Additions and removals are deferred for the whole dispatch, so the callback map stays + // valid for the entire iteration, even on a nested invoke + ++m_invokeDepth; + + for (const auto& [listenerID, callback] : m_callbacks) + { + if (callback && !IsPendingRemoval(listenerID)) + { + callback(p_args...); + } + } + + --m_invokeDepth; + + if (m_invokeDepth == 0) + { + FlushPendingOperations(); + } + } + + template + bool Event::IsPendingRemoval(ListenerID p_listenerID) const + { + return std::find(m_pendingRemovals.begin(), m_pendingRemovals.end(), p_listenerID) != m_pendingRemovals.end(); + } + + template + void Event::FlushPendingOperations() + { + for (const auto listenerID : m_pendingRemovals) + { + m_callbacks.erase(listenerID); + } + + m_pendingRemovals.clear(); + + for (auto& pendingAddition : m_pendingAdditions) + { + m_callbacks.emplace(pendingAddition.first, std::move(pendingAddition.second)); + } + + m_pendingAdditions.clear(); } -} \ 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..704fad8eb 100644 --- a/Sources/OvTools/src/OvTools/Utils/PathParser.cpp +++ b/Sources/OvTools/src/OvTools/Utils/PathParser.cpp @@ -111,12 +111,16 @@ std::string OvTools::Utils::PathParser::FileTypeToString(EFileType p_fileType) OvTools::Utils::PathParser::EFileType OvTools::Utils::PathParser::StringToFileType(const std::string& p_type) { - if (p_type == "Model") return EFileType::MODEL; - if (p_type == "Texture") return EFileType::TEXTURE; - if (p_type == "Shader") return EFileType::SHADER; - if (p_type == "Material") return EFileType::MATERIAL; - if (p_type == "Sound") return EFileType::SOUND; - if (p_type == "Prefab") return EFileType::PREFAB; + if (p_type == "Model") return EFileType::MODEL; + if (p_type == "Texture") return EFileType::TEXTURE; + if (p_type == "Shader") return EFileType::SHADER; + if (p_type == "Shader_Part") return EFileType::SHADER_PART; + if (p_type == "Material") return EFileType::MATERIAL; + if (p_type == "Sound") return EFileType::SOUND; + if (p_type == "Scene") return EFileType::SCENE; + if (p_type == "Prefab") return EFileType::PREFAB; + if (p_type == "Script") return EFileType::SCRIPT; + if (p_type == "Font") return EFileType::FONT; return EFileType::UNKNOWN; } @@ -134,7 +138,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/AWidget.h b/Sources/OvUI/include/OvUI/Widgets/AWidget.h index 648ec4fcb..805ec78c1 100644 --- a/Sources/OvUI/include/OvUI/Widgets/AWidget.h +++ b/Sources/OvUI/include/OvUI/Widgets/AWidget.h @@ -89,7 +89,7 @@ namespace OvUI::Widgets bool neverDisabled = false; // provide a way to bypass the "readonly" setting used by a WidgetContainer protected: - Internal::WidgetContainer* m_parent; + Internal::WidgetContainer* m_parent = nullptr; std::string m_widgetID = "?"; bool m_autoExecutePlugins = true; bool m_skipDisableOverrideLogic = false; diff --git a/Sources/OvUI/include/OvUI/Widgets/Drags/DragMultipleScalars.h b/Sources/OvUI/include/OvUI/Widgets/Drags/DragMultipleScalars.h index 89751461e..4be372c45 100644 --- a/Sources/OvUI/include/OvUI/Widgets/Drags/DragMultipleScalars.h +++ b/Sources/OvUI/include/OvUI/Widgets/Drags/DragMultipleScalars.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include @@ -61,7 +62,63 @@ namespace OvUI::Widgets::Drags values[i] = max; } - if (ImGui::DragScalarN((label + this->m_widgetID).c_str(), m_dataType, values.data(), _Size, speed, &min, &max, format.c_str())) + bool valueChanged = false; + const bool hasDisabledComponent = std::any_of( + disabledComponents.begin(), + disabledComponents.end(), + [](bool p_disabled) { return p_disabled; } + ); + + if (!hasDisabledComponent) + { + valueChanged = ImGui::DragScalarN( + (label + this->m_widgetID).c_str(), + m_dataType, + values.data(), + _Size, + speed, + &min, + &max, + format.c_str() + ); + } + else + { + const float spacing = ImGui::GetStyle().ItemInnerSpacing.x; + const float componentWidth = std::max( + 1.0f, + (ImGui::CalcItemWidth() - spacing * static_cast(_Size - 1)) / static_cast(_Size) + ); + + ImGui::BeginGroup(); + ImGui::PushID((label + this->m_widgetID).c_str()); + for (size_t i = 0; i < _Size; ++i) + { + if (i > 0) + { + ImGui::SameLine(0.0f, spacing); + } + + ImGui::PushID(static_cast(i)); + ImGui::SetNextItemWidth(componentWidth); + ImGui::BeginDisabled(disabledComponents[i]); + valueChanged |= ImGui::DragScalar( + "", + m_dataType, + &values[i], + speed, + &min, + &max, + format.c_str() + ); + ImGui::EndDisabled(); + ImGui::PopID(); + } + ImGui::PopID(); + ImGui::EndGroup(); + } + + if (valueChanged) { ValueChangedEvent.Invoke(values); this->NotifyChange(); @@ -73,6 +130,7 @@ namespace OvUI::Widgets::Drags T max; float speed; std::array values; + std::array disabledComponents{}; std::string label; std::string format; OvTools::Eventing::Event&> ValueChangedEvent; @@ -80,4 +138,4 @@ namespace OvUI::Widgets::Drags protected: ImGuiDataType_ m_dataType; }; -} \ 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..df5d01565 100644 --- a/Sources/OvUI/src/OvUI/Widgets/InputFields/InputText.cpp +++ b/Sources/OvUI/src/OvUI/Widgets/InputFields/InputText.cpp @@ -4,10 +4,28 @@ * @licence: MIT */ +#include +#include #include #include +namespace +{ + int ResizeInputTextBuffer(ImGuiInputTextCallbackData* p_data) + { + if (p_data->EventFlag != ImGuiInputTextFlags_CallbackResize || !p_data->UserData) + { + return 0; + } + + auto& content = *static_cast(p_data->UserData); + content.resize(static_cast(p_data->BufTextLen)); + p_data->Buf = content.data(); + return 0; + } +} + OvUI::Widgets::InputFields::InputText::InputText(const std::string& p_content, const std::string& p_label) : DataWidget(content), content(p_content), label(p_label) { @@ -50,9 +68,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)); - content = content.c_str(); + const auto commonFlags = + (selectAllOnClick ? ImGuiInputTextFlags_AutoSelectAll : ImGuiInputTextFlags_None) | + ImGuiInputTextFlags_CallbackResize; + 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.data(), + content.capacity() + 1, + ImVec2(fieldWidth, fieldHeight), + commonFlags, + ResizeInputTextBuffer, + &content + ); + } + else + { + enterPressed = ImGui::InputText( + (label + m_widgetID).c_str(), + content.data(), + content.capacity() + 1, + ImGuiInputTextFlags_EnterReturnsTrue | commonFlags, + ResizeInputTextBuffer, + &content + ); + } if (content != previousContent) { @@ -60,6 +108,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..42047bdaa 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,24 +55,35 @@ 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)); + auto* widget = widgetsToDraw[index]; + if (!widget || widget->IsDestroyed()) + { + continue; + } if (static_cast(index) == stretchWidget) { ImGui::SetNextItemWidth(-FLT_MIN); } - auto& widget = *m_widgets[index].first; - const auto previousLineBreak = widget.lineBreak; - widget.lineBreak = true; - widget.Draw(); - widget.lineBreak = previousLineBreak; + const auto previousLineBreak = widget->lineBreak; + widget->lineBreak = true; + widget->Draw(); + widget->lineBreak = previousLineBreak; } ImGui::EndTable(); } ImGui::PopStyleVar(); + + --m_drawCallDepth; + + if (m_drawCallDepth == 0) + { + CollectGarbages(); + } }