Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e0c8e55
Inspector elements now keep the same column size consistently based o…
adriengivry Apr 12, 2026
7d22a24
Changed 'Add Component...' button style to default
adriengivry Apr 12, 2026
1dad64d
Table rows with only disabled widgets are now properly skipped
adriengivry Apr 12, 2026
381575e
Added AssetField
adriengivry Apr 12, 2026
d1f5da8
Disabled text input in AssetField
adriengivry Apr 12, 2026
a58af8b
Now using asset icon instead of '...' in AssetField
adriengivry Apr 12, 2026
54238f1
Fixed Windows strncpy issue
adriengivry Apr 12, 2026
5d08947
Neater asset name display
adriengivry Apr 12, 2026
ce1b5c7
No more 'Empty' stirng in asset fields, neater path parsing, and scen…
adriengivry Apr 12, 2026
837960f
Added helper 'GetFriendlyPath' method
adriengivry Apr 13, 2026
903adc1
Fixed inverted UVs for ButtonImage in AssetField
adriengivry Apr 13, 2026
21ad70e
Added support for previews to any asset field, and updated texture fi…
adriengivry Apr 13, 2026
184e77c
Components now use a fixed order, and are re-orderable
adriengivry Apr 13, 2026
24270b8
Fixed inspector component header always re-expanded
adriengivry Apr 13, 2026
09ab86d
Code cleanup
adriengivry Apr 13, 2026
838bc5e
Double click on AssetField to open preview/edit
adriengivry Apr 13, 2026
f02dd0a
AssetBrowser now uses the open providers
adriengivry Apr 13, 2026
3239c6b
Separated GUIDrawer and GUIHelpers
adriengivry Apr 13, 2026
a58585f
Cleaned up GUIDrawer and GUIHelpers
adriengivry Apr 13, 2026
43f3c5d
Fixed behaviour ordering
adriengivry Apr 13, 2026
dc35bd2
Added missing OpenProvider file types
adriengivry Apr 13, 2026
6bc02e7
Enforce path normalization in OpenProvider
adriengivry Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Sources/OvCore/include/OvCore/ECS/Actor.h
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,13 @@ namespace OvCore::ECS
*/
bool RemoveBehaviour(const std::string& p_name);

/**
* Rename a behaviour, preserving its position in the ordering
* @param p_previousName
* @param p_newName
*/
bool RenameBehaviour(const std::string& p_previousName, const std::string& p_newName);

/**
* Try to get the given behaviour (Returns nullptr on failure)
* @param p_name
Expand All @@ -300,6 +307,11 @@ namespace OvCore::ECS
*/
std::unordered_map<std::string, Components::Behaviour>& GetBehaviours();

/**
* Returns the ordered list of behaviour names (display/serialization order)
*/
std::vector<std::string>& GetBehavioursOrder();

/**
* Serialize all the components
*/
Expand Down Expand Up @@ -356,6 +368,7 @@ namespace OvCore::ECS
/* Actors components */
std::vector<std::shared_ptr<Components::AComponent>> m_components;
std::unordered_map<std::string, Components::Behaviour> m_behaviours;
std::vector<std::string> m_behavioursOrder;

public:
Components::CTransform& transform;
Expand Down
4 changes: 2 additions & 2 deletions Sources/OvCore/include/OvCore/ECS/Actor.inl
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ namespace OvCore::ECS

if (auto found = GetComponent<T>(); !found)
{
m_components.insert(m_components.begin(), std::make_shared<T>(*this, p_args...));
T& instance = *dynamic_cast<T*>(m_components.front().get());
m_components.push_back(std::make_shared<T>(*this, p_args...));
T& instance = *dynamic_cast<T*>(m_components.back().get());
ComponentAddedEvent.Invoke(instance);
if (m_playing && IsActive())
{
Expand Down
122 changes: 10 additions & 112 deletions Sources/OvCore/include/OvCore/Helpers/GUIDrawer.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,18 @@

#include <functional>
#include <string>
#include <unordered_set>
#include <vector>

#include <OvMaths/FVector2.h>
#include <OvMaths/FVector3.h>
#include <OvMaths/FVector4.h>
#include <OvMaths/FQuaternion.h>

#include <OvTools/Utils/PathParser.h>

#include <OvUI/Internal/WidgetContainer.h>
#include <OvUI/Widgets/Texts/Text.h>
#include <OvUI/Widgets/Drags/DragSingleScalar.h>
#include <OvUI/Widgets/Drags/DragMultipleScalars.h>
#include <OvUI/Widgets/InputFields/InputText.h>
#include <OvUI/Widgets/Visual/Image.h>
#include <OvUI/Widgets/InputFields/AssetField.h>
#include <OvUI/Types/Color.h>

namespace OvCore::Resources
Expand All @@ -46,116 +42,16 @@ namespace OvRendering::Resources
namespace OvCore::Helpers
{
/**
* Provide some helpers to draw UI elements
* Provides helpers to draw UI elements for common data types and asset resources.
*/
class GUIDrawer
{
public:
/**
* Represents a single item in the picker.
* The key uniquely identifies the item for deduplication when combining lists.
*/
struct PickerItem
{
std::string key; // unique identifier used for deduplication
std::string displayName;
std::string tooltip;
uint32_t iconID = 0;
std::function<void()> onSelected;
};

/**
* An ordered, deduplication-aware collection of picker items.
* Items with the same key are silently dropped when added.
*/
class PickerItemList
{
public:
/**
* Add an item to the list.
* @returns true if added, false if an item with the same key was already present.
*/
bool Add(PickerItem p_item)
{
if (!m_keys.insert(p_item.key).second)
return false;
m_items.push_back(std::move(p_item));
return true;
}

const std::vector<PickerItem>& Items() const { return m_items; }
bool empty() const { return m_items.empty(); }
size_t size() const { return m_items.size(); }

private:
std::vector<PickerItem> m_items;
std::unordered_set<std::string> m_keys;
};

/**
* A callback that builds a PickerItemList for a given file type.
* Called internally by OpenAssetPicker — register once via SetFileItemBuilder.
*/
using FileItemBuilderCallback = std::function<PickerItemList(OvTools::Utils::PathParser::EFileType, std::function<void(std::string)>, bool, bool)>;

using PickerProviderCallback = std::function<void(PickerItemList, std::string)>;

static const OvUI::Types::Color TitleColor;

static const float _MIN_FLOAT;
static const float _MAX_FLOAT;

/**
* Defines the texture to use when there is no texture in a texture resource field
* @param p_emptyTexture
*/
static void ProvideEmptyTexture(OvRendering::Resources::Texture& p_emptyTexture);

/**
* Register the function that builds a PickerItemList for a given file type.
* This is called internally by OpenAssetPicker.
* Call this once during editor startup.
* @param p_builder
*/
static void SetFileItemBuilder(FileItemBuilderCallback p_builder);

/**
* Open the asset picker for the given file type.
* Builds the item list via the registered file item builder, derives the window title
* from the file type, and forwards everything to the registered picker provider.
* Has no effect if either callback has not been registered.
* @param p_fileType
* @param p_onSelect
* @param p_searchProjectFiles Include project assets in the results
* @param p_searchEngineFiles Include engine assets in the results
*/
static void OpenAssetPicker(
OvTools::Utils::PathParser::EFileType p_fileType,
std::function<void(std::string)> p_onSelect,
bool p_searchProjectFiles = true,
bool p_searchEngineFiles = true
);

/**
* Register the function that opens the picker window.
* Call this once during editor startup (typically in Editor::SetupUI).
* @param p_provider
*/
static void SetPickerProvider(PickerProviderCallback p_provider);

/**
* Open the picker with the given list of items.
* Has no effect if no provider has been registered.
* @param p_items
* @param p_title Title displayed in the window's title bar
*/
static void OpenPicker(PickerItemList p_items, std::string p_title);

/**
* Draw a title with the title color
* @param p_root
* @param p_name
*/
static void CreateTitle(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name);

template <typename T>
Expand All @@ -167,12 +63,14 @@ namespace OvCore::Helpers
static void DrawQuat(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvMaths::FQuaternion& p_data, float p_step = 1.f, float p_min = _MIN_FLOAT, float p_max = _MAX_FLOAT);
static void DrawString(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, std::string& p_data);
static void DrawColor(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvUI::Types::Color& p_color, bool p_hasAlpha = false);
static OvUI::Widgets::Texts::Text& DrawMesh(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvRendering::Resources::Model*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::Visual::Image& DrawTexture(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvRendering::Resources::Texture*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::Texts::Text& DrawShader(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvRendering::Resources::Shader*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::Texts::Text& DrawMaterial(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvCore::Resources::Material*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::Texts::Text& DrawSound(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvAudio::Resources::Sound*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::Texts::Text& DrawAsset(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, std::string& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::InputFields::AssetField& DrawMesh(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvRendering::Resources::Model*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::InputFields::AssetField& DrawTexture(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvRendering::Resources::Texture*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::InputFields::AssetField& DrawShader(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvRendering::Resources::Shader*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::InputFields::AssetField& DrawMaterial(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvCore::Resources::Material*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::InputFields::AssetField& DrawSound(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, OvAudio::Resources::Sound*& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);
static OvUI::Widgets::InputFields::AssetField& DrawAsset(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, std::string& p_data, OvTools::Eventing::Event<>* p_updateNotifier = nullptr);

static OvUI::Widgets::InputFields::AssetField& DrawScene(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, std::function<std::string()> p_gatherer, std::function<void(std::string)> p_provider);

template <typename T>
static void DrawScalar(OvUI::Internal::WidgetContainer& p_root, const std::string& p_name, std::function<T(void)> p_gatherer, std::function<void(T)> p_provider, float p_step = 1.f, T p_min = std::numeric_limits<T>::min(), T p_max = std::numeric_limits<T>::max());
Expand Down
86 changes: 86 additions & 0 deletions Sources/OvCore/include/OvCore/Helpers/GUIHelpers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* @project: Overload
* @author: Overload Tech.
* @licence: MIT
*/

#pragma once

#include <cstdint>
#include <functional>
#include <string>
#include <unordered_set>
#include <vector>

#include <OvTools/Utils/PathParser.h>

namespace OvRendering::Resources
{
class Texture;
}

namespace OvCore::Helpers
{
/**
* Asset picking and opening helpers shared across the editor.
* Implements a service locator pattern for UI operations.
*/
class GUIHelpers
{
public:
struct PickerItem
{
std::string key;
std::string displayName;
std::string tooltip;
uint32_t iconID = 0;
std::function<void()> onSelected;
};

class PickerItemList
{
public:
bool Add(PickerItem p_item)
{
if (!m_keys.insert(p_item.key).second)
return false;
m_items.push_back(std::move(p_item));
return true;
}

const std::vector<PickerItem>& Items() const { return m_items; }
bool empty() const { return m_items.empty(); }
size_t size() const { return m_items.size(); }

private:
std::vector<PickerItem> m_items;
std::unordered_set<std::string> m_keys;
};

using FileItemBuilderCallback = std::function<PickerItemList(OvTools::Utils::PathParser::EFileType, std::function<void(std::string)>, bool, bool)>;
using OpenProviderCallback = std::function<void(const std::string&)>;
using PickerProviderCallback = std::function<void(PickerItemList, std::string)>;
using IconProviderCallback = std::function<uint32_t(OvTools::Utils::PathParser::EFileType)>;

static void ProvideEmptyTexture(OvRendering::Resources::Texture& p_emptyTexture);
static OvRendering::Resources::Texture* GetEmptyTexture();

static void SetFileItemBuilder(FileItemBuilderCallback p_builder);

static void OpenAssetPicker(
OvTools::Utils::PathParser::EFileType p_fileType,
std::function<void(std::string)> p_onSelect,
bool p_searchProjectFiles = true,
bool p_searchEngineFiles = true
);

static void SetOpenProvider(OpenProviderCallback p_provider);
static void Open(const std::string& p_path);

static void SetIconProvider(IconProviderCallback p_provider);
static uint32_t GetIconForFileType(OvTools::Utils::PathParser::EFileType p_fileType);

static void SetPickerProvider(PickerProviderCallback p_provider);
static void OpenPicker(PickerItemList p_items, std::string p_title);
};
}
49 changes: 45 additions & 4 deletions Sources/OvCore/src/OvCore/ECS/Actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ std::vector<std::shared_ptr<OvCore::ECS::Components::AComponent>>& OvCore::ECS::
OvCore::ECS::Components::Behaviour & OvCore::ECS::Actor::AddBehaviour(const std::string & p_name)
{
m_behaviours.try_emplace(p_name, *this, p_name);
m_behavioursOrder.push_back(p_name);
Components::Behaviour& newInstance = m_behaviours.at(p_name);
BehaviourAddedEvent.Invoke(newInstance);
if (m_playing && IsActive())
Expand Down Expand Up @@ -371,14 +372,45 @@ bool OvCore::ECS::Actor::RemoveBehaviour(const std::string & p_name)
if (found)
{
BehaviourRemovedEvent.Invoke(*found);
return m_behaviours.erase(p_name);
m_behaviours.erase(p_name);
auto it = std::find(m_behavioursOrder.begin(), m_behavioursOrder.end(), p_name);
if (it != m_behavioursOrder.end())
m_behavioursOrder.erase(it);
return true;
}
else
{
return false;
}
}

bool OvCore::ECS::Actor::RenameBehaviour(const std::string& p_previousName, const std::string& p_newName)
{
auto orderIt = std::find(m_behavioursOrder.begin(), m_behavioursOrder.end(), p_previousName);
if (orderIt == m_behavioursOrder.end())
return false;

Components::Behaviour* found = GetBehaviour(p_previousName);
if (!found)
return false;

BehaviourRemovedEvent.Invoke(*found);
m_behaviours.erase(p_previousName);

*orderIt = p_newName;

m_behaviours.try_emplace(p_newName, *this, p_newName);
Components::Behaviour& newInstance = m_behaviours.at(p_newName);
BehaviourAddedEvent.Invoke(newInstance);
if (m_playing && IsActive())
{
newInstance.OnAwake();
newInstance.OnEnable();
newInstance.OnStart();
}
return true;
}

OvCore::ECS::Components::Behaviour* OvCore::ECS::Actor::GetBehaviour(const std::string& p_name)
{
if (auto result = m_behaviours.find(p_name); result != m_behaviours.end())
Expand All @@ -392,6 +424,11 @@ std::unordered_map<std::string, OvCore::ECS::Components::Behaviour>& OvCore::ECS
return m_behaviours;
}

std::vector<std::string>& OvCore::ECS::Actor::GetBehavioursOrder()
{
return m_behavioursOrder;
}

void OvCore::ECS::Actor::OnSerialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode * p_actorsRoot)
{
tinyxml2::XMLNode* actorNode = p_doc.NewElement("actor");
Expand Down Expand Up @@ -426,21 +463,25 @@ void OvCore::ECS::Actor::OnSerialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XM
tinyxml2::XMLNode* behavioursNode = p_doc.NewElement("behaviours");
actorNode->InsertEndChild(behavioursNode);

for (auto& behaviour : m_behaviours)
for (auto& name : m_behavioursOrder)
{
auto it = m_behaviours.find(name);
if (it == m_behaviours.end()) continue;
auto& behaviour = it->second;

/* Current behaviour root */
tinyxml2::XMLNode* behaviourNode = p_doc.NewElement("behaviour");
behavioursNode->InsertEndChild(behaviourNode);

/* Behaviour type */
OvCore::Helpers::Serializer::SerializeString(p_doc, behaviourNode, "type", behaviour.first);
OvCore::Helpers::Serializer::SerializeString(p_doc, behaviourNode, "type", name);

/* Data node (Will be passed to the behaviour) */
tinyxml2::XMLElement* data = p_doc.NewElement("data");
behaviourNode->InsertEndChild(data);

/* Data serialization of the behaviour */
behaviour.second.OnSerialize(p_doc, data);
behaviour.OnSerialize(p_doc, data);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ std::array<OvUI::Widgets::AWidget*, 2> CustomMaterialDrawer(OvUI::Internal::Widg
const size_t before = p_root.GetWidgets().size();
OvCore::Helpers::GUIDrawer::DrawMaterial(p_root, p_name, p_data, nullptr);
auto& widgets = p_root.GetWidgets();
// DrawMaterial adds exactly 2 widgets: [before]=TextColored title, [before+1]=Group rightSide
// DrawMaterial adds exactly 2 widgets: [before]=TextColored title, [before+1]=AssetField
return { widgets[before].first, widgets[before + 1].first };
}

Expand Down
Loading