From 0f62a95b9d2d34b2ab04ac0b654fc1d36e4b886a Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 1 Dec 2025 02:40:04 +0000 Subject: [PATCH 01/23] Attempt to fix datamodel issues --- src/bind/Context.cpp | 347 ++++++++++++++++----------------- src/bind/DataModel.cpp | 53 ++--- src/plugin/SolLuaDataModel.cpp | 250 ++++++++++++------------ src/plugin/SolLuaDataModel.h | 74 ++++--- 4 files changed, 363 insertions(+), 361 deletions(-) diff --git a/src/bind/Context.cpp b/src/bind/Context.cpp index 9006499..38f2405 100644 --- a/src/bind/Context.cpp +++ b/src/bind/Context.cpp @@ -4,190 +4,179 @@ #include #include -#include #include +#include #include SOLHPP #include "bind.h" -#include "plugin/SolLuaDocument.h" #include "plugin/SolLuaDataModel.h" +#include "plugin/SolLuaDocument.h" - -namespace Rml::SolLua -{ - - namespace document - { - /// - /// Return a SolLuaDocument. - /// - static auto getDocumentBypass(Rml::Context& self, int idx) - { - auto document = self.GetDocument(idx); - auto result = dynamic_cast(document); - return result; - } - - /// - /// Return a SolLuaDocument. - /// - static auto getDocumentBypassString(Rml::Context& self, const Rml::String& name) - { - auto document = self.GetDocument(name); - return dynamic_cast(document); - } - - /// - /// Helper function to fill the indexed table with data. - /// - static auto getDocument(Rml::Context& self) - { - std::function result = [&self](int idx) -> auto { return getDocumentBypass(self, idx); }; - return result; - } - } - - namespace datamodel - { - /// - /// Bind a sol::table into the data model. - /// - /// The data model container. - /// The table to bind. - static void bindTable(SolLuaDataModel* data, sol::table& table) - { - for (auto& [key, value] : table) - { - auto skey = key.as(); - auto it = data->ObjectList.insert_or_assign(skey, value); - - if (value.get_type() == sol::type::function) - { - data->Constructor.BindEventCallback(skey, - [skey, cb = sol::protected_function{ value }, state = sol::state_view{ table.lua_state() }](Rml::DataModelHandle, Rml::Event& event, const Rml::VariantList& varlist) - { - if (cb.valid()) - { - std::vector args; - for (const auto& variant : varlist) - { - args.push_back(makeObjectFromVariant(&variant, state)); - } - auto pfr = cb(event, sol::as_args(args)); - if (!pfr.valid()) - ErrorHandler(cb.lua_state(), std::move(pfr)); - } - }); - } - else - { - data->Constructor.BindCustomDataVariable(skey, Rml::DataVariable(data->ObjectDef.get(), &(it.first->second))); - } - } - } - - /// - /// Opens a Lua data model. - /// - /// The context that called this function. - /// The name of the data model. - /// The table to bind as the data model. - /// Lua state. - /// A unique pointer to a Sol Lua Data Model. - static std::unique_ptr openDataModel(Rml::Context& self, const Rml::String& name, sol::object model, sol::this_state s) - { - sol::state_view lua{ s }; - - // Create data model. - auto constructor = self.CreateDataModel(name); - auto data = std::make_unique(lua); - - // Already created? Get existing. - if (!constructor) - { - constructor = self.GetDataModel(name); - if (!constructor) - return data; - } - - data->Constructor = constructor; - data->Handle = constructor.GetModelHandle(); - data->ObjectDef = std::make_unique(data.get()); - - // Only bind table. - if (model.get_type() == sol::type::table) - { - data->Table = model.as(); - datamodel::bindTable(data.get(), data->Table); - } - - return data; - } - } - - namespace element - { - static auto getElementAtPoint1(Rml::Context& self, Rml::Vector2f point) - { - return self.GetElementAtPoint(point); - } - - static auto getElementAtPoint2(Rml::Context& self, Rml::Vector2f point, Rml::Element& ignore) - { - return self.GetElementAtPoint(point, &ignore); - } - } - - /// - /// Binds the Rml::Context class to Lua. - /// - /// The Lua state to bind into. - void bind_context(sol::state_view& lua) - { - lua.new_usertype("Context", sol::no_constructor, - // M - "AddEventListener", &Rml::Context::AddEventListener, - "CreateDocument", [](Rml::Context& self) { return self.CreateDocument(); }, - "LoadDocument", [](Rml::Context& self, const Rml::String& document) { - auto doc = self.LoadDocument(document); - return dynamic_cast(doc); - }, - "GetDocument", &document::getDocumentBypassString, - "Render", &Rml::Context::Render, - "UnloadAllDocuments", &Rml::Context::UnloadAllDocuments, - "UnloadDocument", &Rml::Context::UnloadDocument, - "Update", &Rml::Context::Update, - "OpenDataModel", &datamodel::openDataModel, - "ProcessMouseMove", &Rml::Context::ProcessMouseMove, - "ProcessMouseButtonDown", &Rml::Context::ProcessMouseButtonDown, - "ProcessMouseButtonUp", &Rml::Context::ProcessMouseButtonUp, - "ProcessMouseWheel", sol::resolve(&Rml::Context::ProcessMouseWheel), - "ProcessMouseLeave", &Rml::Context::ProcessMouseLeave, - "IsMouseInteracting", &Rml::Context::IsMouseInteracting, - "ProcessKeyDown", &Rml::Context::ProcessKeyDown, - "ProcessKeyUp", &Rml::Context::ProcessKeyUp, - "ProcessTextInput", sol::resolve(&Rml::Context::ProcessTextInput), - //-- - "EnableMouseCursor", &Rml::Context::EnableMouseCursor, - "ActivateTheme", &Rml::Context::ActivateTheme, - "IsThemeActive", &Rml::Context::IsThemeActive, - "GetElementAtPoint", sol::overload(&element::getElementAtPoint1, &element::getElementAtPoint2), - "PullDocumentToFront", &Rml::Context::PullDocumentToFront, - "PushDocumentToBack", &Rml::Context::PushDocumentToBack, - "UnfocusDocument", &Rml::Context::UnfocusDocument, - // RemoveEventListener - - // G+S - "dimensions", sol::property(&Rml::Context::GetDimensions, &Rml::Context::SetDimensions), - "dp_ratio", sol::property(&Rml::Context::GetDensityIndependentPixelRatio, &Rml::Context::SetDensityIndependentPixelRatio), - - // G - "documents", sol::readonly_property(&getIndexedTable), - "focus_element", sol::readonly_property(&Rml::Context::GetFocusElement), - "hover_element", sol::readonly_property(&Rml::Context::GetHoverElement), - "name", sol::readonly_property(&Rml::Context::GetName), - "root_element", sol::readonly_property(&Rml::Context::GetRootElement) - ); - } +namespace Rml::SolLua { + +namespace document { +/// +/// Return a SolLuaDocument. +/// +static auto getDocumentBypass(Rml::Context &self, int idx) { + auto document = self.GetDocument(idx); + auto result = dynamic_cast(document); + return result; +} + +/// +/// Return a SolLuaDocument. +/// +static auto getDocumentBypassString(Rml::Context &self, const Rml::String &name) { + auto document = self.GetDocument(name); + return dynamic_cast(document); +} + +/// +/// Helper function to fill the indexed table with data. +/// +static auto getDocument(Rml::Context &self) { + std::function result = [&self](int idx) -> auto { + return getDocumentBypass(self, idx); + }; + return result; +} +} + +namespace datamodel { + +/// +/// Opens a Lua data model. +/// +/// The context that called this function. +/// The name of the data model. +/// The table to bind as the data model. +/// Lua state. +/// A unique pointer to a Sol Lua Data Model. +static std::shared_ptr +openDataModel(Rml::Context &self, const Rml::String &name, sol::object model, sol::this_state s) { + if (model.get_type() != sol::type::table) { + Rml::Log::Message(Log::LT_ERROR, "Data model must be a table."); + return nullptr; + } + + // Create data model. + auto constructor = self.CreateDataModel(name); + + // Already created? Get existing. + if (!constructor) { + constructor = self.GetDataModel(name); + if (!constructor) { + return nullptr; + } + } + + auto dataModel = std::make_shared(model.as(), constructor); + + // Alias data model to it's top level proxy. + return { dataModel, &dataModel->topLevelProxy() }; +} +} + +namespace element { +static auto getElementAtPoint1(Rml::Context &self, Rml::Vector2f point) { + return self.GetElementAtPoint(point); +} + +static auto getElementAtPoint2(Rml::Context &self, Rml::Vector2f point, Rml::Element &ignore) { + return self.GetElementAtPoint(point, &ignore); +} +} + +/// +/// Binds the Rml::Context class to Lua. +/// +/// The Lua state to bind into. +void bind_context(sol::state_view &lua) { + lua.new_usertype( + "Context", + sol::no_constructor, + // M + "AddEventListener", + &Rml::Context::AddEventListener, + "CreateDocument", + [](Rml::Context &self) { + return self.CreateDocument(); + }, + "LoadDocument", + [](Rml::Context &self, const Rml::String &document) { + auto doc = self.LoadDocument(document); + return dynamic_cast(doc); + }, + "GetDocument", + &document::getDocumentBypassString, + "Render", + &Rml::Context::Render, + "UnloadAllDocuments", + &Rml::Context::UnloadAllDocuments, + "UnloadDocument", + &Rml::Context::UnloadDocument, + "Update", + &Rml::Context::Update, + "OpenDataModel", + &datamodel::openDataModel, + "ProcessMouseMove", + &Rml::Context::ProcessMouseMove, + "ProcessMouseButtonDown", + &Rml::Context::ProcessMouseButtonDown, + "ProcessMouseButtonUp", + &Rml::Context::ProcessMouseButtonUp, + "ProcessMouseWheel", + sol::resolve(&Rml::Context::ProcessMouseWheel), + "ProcessMouseLeave", + &Rml::Context::ProcessMouseLeave, + "IsMouseInteracting", + &Rml::Context::IsMouseInteracting, + "ProcessKeyDown", + &Rml::Context::ProcessKeyDown, + "ProcessKeyUp", + &Rml::Context::ProcessKeyUp, + "ProcessTextInput", + sol::resolve(&Rml::Context::ProcessTextInput), + //-- + "EnableMouseCursor", + &Rml::Context::EnableMouseCursor, + "ActivateTheme", + &Rml::Context::ActivateTheme, + "IsThemeActive", + &Rml::Context::IsThemeActive, + "GetElementAtPoint", + sol::overload(&element::getElementAtPoint1, &element::getElementAtPoint2), + "PullDocumentToFront", + &Rml::Context::PullDocumentToFront, + "PushDocumentToBack", + &Rml::Context::PushDocumentToBack, + "UnfocusDocument", + &Rml::Context::UnfocusDocument, + // RemoveEventListener + + // G+S + "dimensions", + sol::property(&Rml::Context::GetDimensions, &Rml::Context::SetDimensions), + "dp_ratio", + sol::property(&Rml::Context::GetDensityIndependentPixelRatio, &Rml::Context::SetDensityIndependentPixelRatio), + + // G + "documents", + sol::readonly_property(&getIndexedTable< + SolLuaDocument, + Rml::Context, + &document::getDocument, + &Rml::Context::GetNumDocuments>), + "focus_element", + sol::readonly_property(&Rml::Context::GetFocusElement), + "hover_element", + sol::readonly_property(&Rml::Context::GetHoverElement), + "name", + sol::readonly_property(&Rml::Context::GetName), + "root_element", + sol::readonly_property(&Rml::Context::GetRootElement) + ); +} } // end namespace Rml::SolLua diff --git a/src/bind/DataModel.cpp b/src/bind/DataModel.cpp index 453e34c..eee1347 100644 --- a/src/bind/DataModel.cpp +++ b/src/bind/DataModel.cpp @@ -5,31 +5,32 @@ #include "plugin/SolLuaDataModel.h" - -namespace Rml::SolLua -{ - namespace functions - { - static sol::object dataModelGet(SolLuaDataModel& self, const std::string& name, sol::this_state s) - { - return self.Table.get(name); - } - - static void dataModelSet(SolLuaDataModel& self, const std::string& name, sol::object value, sol::this_state s) - { - self.Handle.DirtyVariable(name); - self.Table.set(name, value); - } - } - - void bind_datamodel(sol::state_view& lua) - { - - lua.new_usertype("SolLuaDataModel", sol::no_constructor, - sol::meta_function::index, &functions::dataModelGet, - sol::meta_function::new_index, &functions::dataModelSet - ); - - } +namespace Rml::SolLua { +namespace functions { +static sol::object dataModelGet(SolLuaDataModelTableProxy &self, const std::string &name, sol::this_state s) { + auto proxyTable = self.children.find(name); + if (proxyTable != self.children.end()) { + return sol::make_object(s, &proxyTable->second); + } + return self.objectDef->table().get(name); +} + +static void +dataModelSet(SolLuaDataModelTableProxy &self, const std::string &name, sol::object value, sol::this_state s) { + self.objectDef->table().set(name, value); + self.modelHandle.DirtyVariable(name); +} +} + +void bind_datamodel(sol::state_view &lua) { + lua.new_usertype( + "SolLuaDataModelTableProxy", + sol::no_constructor, + sol::meta_function::index, + &functions::dataModelGet, + sol::meta_function::new_index, + &functions::dataModelSet + ); +} } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index a2c1aaa..8caf2da 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -1,133 +1,133 @@ #include -#include #include +#include #include SOLHPP #include "SolLuaDataModel.h" - - -namespace Rml::SolLua -{ - SolLuaDataModel::SolLuaDataModel(sol::state_view s) : Lua{ s } {} - - SolLuaObjectDef::SolLuaObjectDef(SolLuaDataModel* model) - : VariableDefinition(DataVariableType::Scalar), m_model(model) - { - } - - bool SolLuaObjectDef::Get(void* ptr, Rml::Variant& variant) - { - auto obj = static_cast(ptr); - - if (obj->is()) - variant = obj->as(); - else if (obj->is()) - variant = obj->as(); - else if (obj->is()) - variant = obj->as(); - else if (obj->is()) - variant = obj->as(); - else if (obj->is()) - variant = obj->as(); - else if (obj->is()) - variant = obj->as(); - else if (obj->is()) - variant = obj->as(); - else // if (obj->get_type() == sol::type::lua_nil) - variant = Rml::Variant{}; - - return true; - } - - bool SolLuaObjectDef::Set(void* ptr, const Rml::Variant& variant) - { - auto obj = static_cast(ptr); - - if (obj->is()) - variant.GetInto(*static_cast(ptr)); - else if (obj->is()) - variant.GetInto(*static_cast(ptr)); - else if (obj->is()) - variant.GetInto(*static_cast(ptr)); - else if (obj->is()) - variant.GetInto(*static_cast(ptr)); - else if (obj->is()) - variant.GetInto(*static_cast(ptr)); - else if (obj->is()) - variant.GetInto(*static_cast(ptr)); - else if (obj->is()) - variant.GetInto(*static_cast(ptr)); - else // if (obj->get_type() == sol::type::lua_nil) - *obj = sol::make_object(m_model->Lua, sol::nil); - - return true; - } - - int SolLuaObjectDef::Size(void* ptr) - { - // Non-table types are 1 entry long. - auto object = static_cast(ptr); - if (object->get_type() != sol::type::table) - return 1; - - auto t = object->as(); - return static_cast(t.size()); - } - - DataVariable SolLuaObjectDef::Child(void* ptr, const Rml::DataAddressEntry& address) - { - // Child should be called on a table. - auto object = static_cast(ptr); - if (object->get_type() != sol::type::table) - return DataVariable{}; - - // Get our table object. - // Get the pointer as a string for use with holding onto the object. - auto table = object->as(); - std::string tablestr = std::to_string(reinterpret_cast(table.pointer())); - - // Accessing by name. - if (address.index == -1) - { - // Try to get the object. - auto e = table.get(address.name); - if (e.get_type() == sol::type::lua_nil) - return DataVariable{}; - - // Hold a reference to it and return the pointer. - auto it = m_model->ObjectList.insert_or_assign(tablestr + "_" + std::to_string(address.index), e); - return DataVariable{ m_model->ObjectDef.get(), &(it.first->second) }; - } - // Accessing by index. - else - { - // See if we have a key with the index. - auto has_index = table.get(address.index); - if (has_index.get_type() != sol::type::lua_nil) - { - auto it = m_model->ObjectList.insert_or_assign(tablestr + "_" + std::to_string(address.index), has_index); - return DataVariable{ m_model->ObjectDef.get(), &(it.first->second) }; - } - - // Iterate through the entries and grab the nth entry. - int idx = 0; - for (auto& [k, v] : table.pairs()) - { - if (idx == address.index) - { - auto it = m_model->ObjectList.insert_or_assign(tablestr + "_" + std::to_string(idx), v); - return DataVariable{ m_model->ObjectDef.get(), &(it.first->second) }; - } - ++idx; - } - - // Index out of range. - return DataVariable{}; - } - - // Failure. - return DataVariable{}; - } +#include "SolLuaDocument.h" +#include "bind/bind.h" + +namespace Rml::SolLua { +namespace {} + +SolLuaDataModel::SolLuaDataModel(const sol::table &model, const Rml::DataModelConstructor &constructor) + : m_constructor(constructor), + m_topLevelProxy{ .modelHandle = m_constructor.GetModelHandle(), + .objectDef = std::make_unique(model) } { + wrapTable(m_topLevelProxy, true); +} + +SolLuaDataModelTableProxy &SolLuaDataModel::topLevelProxy() { + return m_topLevelProxy; +} + +void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) { + for (auto &[key, value] : proxy.objectDef->table()) { + auto skey = key.as(); + + if (value.get_type() == sol::type::table) { + auto childProxyIt = proxy.children.emplace( + skey, + SolLuaDataModelTableProxy{ .modelHandle = m_topLevelProxy.modelHandle, + .objectDef = std::make_unique(value.as()) } + ); + assert(childProxyIt.second); + wrapTable(childProxyIt.first->second, false); + } else { + if (value.get_type() == sol::type::function) { + if (!topLevel) { + Rml::Log::Message( + Log::LT_ERROR, "Event callbacks are only allowed at the top level of a data model." + ); + continue; + } + + m_constructor.BindEventCallback( + skey, + [cb = sol::protected_function{ value }, + state = sol::state_view{ proxy.objectDef->table().lua_state( + ) }](Rml::DataModelHandle, Rml::Event &event, const Rml::VariantList &varlist) { + if (cb.valid()) { + std::vector args; + for (const auto &variant : varlist) { + args.push_back(makeObjectFromVariant(&variant, state)); + } + auto pfr = cb(event, sol::as_args(args)); + if (!pfr.valid()) { + ErrorHandler(cb.lua_state(), std::move(pfr)); + } + } + } + ); + } else { + auto it = proxy.keys.emplace(skey); + m_constructor.BindCustomDataVariable( + skey, Rml::DataVariable(proxy.objectDef.get(), const_cast(it.first->data())) + ); + } + } + } +} + +/// SolLuaObjectDef +SolLuaObjectDef::SolLuaObjectDef(sol::table table) + : VariableDefinition(DataVariableType::Scalar), + m_table(std::move(table)) { +} + +bool SolLuaObjectDef::Get(void *ptr, Rml::Variant &variant) { + auto *key = const_cast(static_cast(ptr)); + sol::object obj = m_table[key]; + + if (obj.is()) { + variant = obj.as(); + } else if (obj.is()) { + variant = obj.as(); + } else if (obj.is()) { + variant = obj.as(); + } else if (obj.is()) { + variant = obj.as(); + } else if (obj.is()) { + variant = obj.as(); + } else if (obj.is()) { + variant = obj.as(); + } else if (obj.is()) { + variant = obj.is(); + } else { + variant = Rml::Variant{}; + } + + return true; +} + +bool SolLuaObjectDef::Set(void *ptr, const Rml::Variant &variant) { + auto *key = const_cast(static_cast(ptr)); + sol::table_proxy obj = m_table[key]; + obj = makeObjectFromVariant(&variant, m_table.lua_state()); + return true; +} + +int SolLuaObjectDef::Size(void *ptr) { + // Non-table types are 1 entry long. + auto object = static_cast(ptr); + if (object->get_type() != sol::type::table) { + return 1; + } + + auto t = object->as(); + return static_cast(t.size()); +} + +DataVariable SolLuaObjectDef::Child(void *ptr, const Rml::DataAddressEntry &address) { + __debugbreak(); // TODO: Implement Child access for SolLua objects. + + // Failure. + return DataVariable{}; +} + +sol::table &SolLuaObjectDef::table() { + return m_table; +} } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index 0d88fb2..716ad52 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -3,41 +3,53 @@ #include #include #include +#include -#include #include +#include #include SOLHPP +namespace Rml::SolLua { +class SolLuaObjectDef; + +/// Userdata that proxies any table in the model, recursively +struct SolLuaDataModelTableProxy { + Rml::DataModelHandle modelHandle; + std::unique_ptr objectDef; + + // Store keys in a set to keep alive the strings + std::unordered_set keys; + + // Children proxies for nested tables + std::unordered_map children; +}; + +class SolLuaDataModel { + public: + SolLuaDataModel(const sol::table &model, const Rml::DataModelConstructor &constructor); + + SolLuaDataModelTableProxy &topLevelProxy(); + + private: + void wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel); + + Rml::DataModelConstructor m_constructor; + + SolLuaDataModelTableProxy m_topLevelProxy; +}; + +class SolLuaObjectDef final : public Rml::VariableDefinition { + public: + SolLuaObjectDef(sol::table table); + bool Get(void *ptr, Rml::Variant &variant) override; + bool Set(void *ptr, const Rml::Variant &variant) override; + int Size(void *ptr) override; + DataVariable Child(void *ptr, const Rml::DataAddressEntry &address) override; + + sol::table &table(); -namespace Rml::SolLua -{ - class SolLuaObjectDef; - - struct SolLuaDataModel - { - SolLuaDataModel(sol::state_view s); - - Rml::DataModelConstructor Constructor; - Rml::DataModelHandle Handle; - sol::state_view Lua; - std::unique_ptr ObjectDef; - - // sol data types are reference counted. Hold onto them as we use them. - sol::table Table; - std::unordered_map ObjectList; - }; - - class SolLuaObjectDef final : public Rml::VariableDefinition - { - public: - SolLuaObjectDef(SolLuaDataModel* model); - bool Get(void* ptr, Rml::Variant& variant) override; - bool Set(void* ptr, const Rml::Variant& variant) override; - int Size(void* ptr) override; - DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override; - protected: - SolLuaDataModel* m_model; - sol::object m_object; - }; + protected: + sol::table m_table; +}; } // end namespace Rml::SolLua From a7e08c0c7e10920a5eb8962ef6be0ab613f228c6 Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 1 Dec 2025 05:37:06 +0000 Subject: [PATCH 02/23] Support `Child` --- src/plugin/SolLuaDataModel.cpp | 108 +++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 19 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 8caf2da..9dba203 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -9,7 +9,26 @@ #include "bind/bind.h" namespace Rml::SolLua { -namespace {} +namespace { + +class LiteralIntDefinition final : public VariableDefinition { + public: + LiteralIntDefinition() + : VariableDefinition(DataVariableType::Scalar) { + } + + bool Get(void *ptr, Variant &variant) override { + variant = static_cast(reinterpret_cast(ptr)); + return true; + } +}; + +DataVariable MakeLiteralIntVariable(int value) { + static LiteralIntDefinition literal_int_definition; + return DataVariable(&literal_int_definition, reinterpret_cast(static_cast(value))); +} + +} SolLuaDataModel::SolLuaDataModel(const sol::table &model, const Rml::DataModelConstructor &constructor) : m_constructor(constructor), @@ -24,7 +43,16 @@ SolLuaDataModelTableProxy &SolLuaDataModel::topLevelProxy() { void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) { for (auto &[key, value] : proxy.objectDef->table()) { - auto skey = key.as(); + std::string skey; + if (key.get_type() == sol::type::string) { + skey = key.as(); + } else if (key.get_type() == sol::type::number) { + // Assign a pseudo-key for numeric indices + // TODO: check if the number is an integer? + skey = std::format("[{}]", key.as() - 1); // Lua is 1-based + } else { + Rml::Log::Message(Log::LT_ERROR, "Data model key with type other than string or integer is unsupported"); + } if (value.get_type() == sol::type::table) { auto childProxyIt = proxy.children.emplace( @@ -34,6 +62,12 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) ); assert(childProxyIt.second); wrapTable(childProxyIt.first->second, false); + if (skey[0] != '[') { + // Skip pseudo-keys - they will be handled in `Child` + m_constructor.BindCustomDataVariable( + skey, Rml::DataVariable(childProxyIt.first->second.objectDef.get(), &childProxyIt.first->second) + ); + } } else { if (value.get_type() == sol::type::function) { if (!topLevel) { @@ -62,9 +96,12 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) ); } else { auto it = proxy.keys.emplace(skey); - m_constructor.BindCustomDataVariable( - skey, Rml::DataVariable(proxy.objectDef.get(), const_cast(it.first->data())) - ); + if (skey[0] != '[') { + // Skip pseudo-keys - they will be handled in `Child` + m_constructor.BindCustomDataVariable( + skey, Rml::DataVariable(proxy.objectDef.get(), const_cast(it.first->data())) + ); + } } } } @@ -77,8 +114,15 @@ SolLuaObjectDef::SolLuaObjectDef(sol::table table) } bool SolLuaObjectDef::Get(void *ptr, Rml::Variant &variant) { + sol::object obj; auto *key = const_cast(static_cast(ptr)); - sol::object obj = m_table[key]; + if (key[0] == '[') { + // Pseudo-key: access by index + int idx = std::atoi(key + 1); + obj = m_table[idx + 1]; // Lua is 1-based + } else { + obj = m_table[key]; + } if (obj.is()) { variant = obj.as(); @@ -103,27 +147,53 @@ bool SolLuaObjectDef::Get(void *ptr, Rml::Variant &variant) { bool SolLuaObjectDef::Set(void *ptr, const Rml::Variant &variant) { auto *key = const_cast(static_cast(ptr)); - sol::table_proxy obj = m_table[key]; - obj = makeObjectFromVariant(&variant, m_table.lua_state()); + if (key[0] == '[') { + // Pseudo-key: access by index + int idx = std::atoi(key + 1); + sol::table_proxy obj = m_table[idx + 1]; // Lua is 1-based + obj = makeObjectFromVariant(&variant, m_table.lua_state()); + } else { + sol::table_proxy obj = m_table[key]; + obj = makeObjectFromVariant(&variant, m_table.lua_state()); + } return true; } int SolLuaObjectDef::Size(void *ptr) { - // Non-table types are 1 entry long. - auto object = static_cast(ptr); - if (object->get_type() != sol::type::table) { - return 1; - } - - auto t = object->as(); - return static_cast(t.size()); + // TODO: can size be called on non-proxy objects? + SolLuaDataModelTableProxy &proxy = *static_cast(ptr); + return static_cast(proxy.objectDef->table().size()); } DataVariable SolLuaObjectDef::Child(void *ptr, const Rml::DataAddressEntry &address) { - __debugbreak(); // TODO: Implement Child access for SolLua objects. + SolLuaDataModelTableProxy &proxy = *static_cast(ptr); + + std::string skey; + sol::object obj; + if (address.index != -1) { + // Access by index + skey = std::format("[{}]", address.index); + obj = proxy.objectDef->table()[address.index + 1]; // Lua is 1-based + } else { + if (address.name == "size") { + return MakeLiteralIntVariable(proxy.objectDef->table().size()); + } + + skey = address.name; + obj = proxy.objectDef->table()[address.name]; + } + + if (obj.get_type() == sol::type::table) { + auto it = proxy.children.find(skey); + assert(it != proxy.children.end()); + + // Pass proxy as ptr to be used in `Child` calls further down the chain + return { it->second.objectDef.get(), &it->second }; + } - // Failure. - return DataVariable{}; + auto it = proxy.keys.find(skey); + assert(it != proxy.keys.end()); + return { proxy.objectDef.get(), const_cast(it->data()) }; } sol::table &SolLuaObjectDef::table() { From e4043bfd5dc1f42b012e2891d75208d891ff3a5a Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 1 Dec 2025 06:10:19 +0000 Subject: [PATCH 03/23] Couple of fixes --- src/plugin/SolLuaDataModel.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 9dba203..9f8a9d6 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -72,7 +72,7 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) if (value.get_type() == sol::type::function) { if (!topLevel) { Rml::Log::Message( - Log::LT_ERROR, "Event callbacks are only allowed at the top level of a data model." + Log::LT_WARNING, "Event callbacks are only allowed at the top level of a data model." ); continue; } @@ -119,6 +119,10 @@ bool SolLuaObjectDef::Get(void *ptr, Rml::Variant &variant) { if (key[0] == '[') { // Pseudo-key: access by index int idx = std::atoi(key + 1); + if (idx < 0 || idx >= m_table.size()) { + Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); + return false; + } obj = m_table[idx + 1]; // Lua is 1-based } else { obj = m_table[key]; @@ -137,7 +141,7 @@ bool SolLuaObjectDef::Get(void *ptr, Rml::Variant &variant) { } else if (obj.is()) { variant = obj.as(); } else if (obj.is()) { - variant = obj.is(); + variant = obj.as(); } else { variant = Rml::Variant{}; } @@ -150,6 +154,10 @@ bool SolLuaObjectDef::Set(void *ptr, const Rml::Variant &variant) { if (key[0] == '[') { // Pseudo-key: access by index int idx = std::atoi(key + 1); + if (idx < 0 || idx >= m_table.size()) { + Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); + return false; + } sol::table_proxy obj = m_table[idx + 1]; // Lua is 1-based obj = makeObjectFromVariant(&variant, m_table.lua_state()); } else { @@ -167,16 +175,21 @@ int SolLuaObjectDef::Size(void *ptr) { DataVariable SolLuaObjectDef::Child(void *ptr, const Rml::DataAddressEntry &address) { SolLuaDataModelTableProxy &proxy = *static_cast(ptr); + sol::table t = proxy.objectDef->table(); std::string skey; sol::object obj; if (address.index != -1) { + if (address.index < 0 || address.index >= t.size()) { + Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); + return {}; + } // Access by index skey = std::format("[{}]", address.index); - obj = proxy.objectDef->table()[address.index + 1]; // Lua is 1-based + obj = t[address.index + 1]; // Lua is 1-based } else { if (address.name == "size") { - return MakeLiteralIntVariable(proxy.objectDef->table().size()); + return MakeLiteralIntVariable(t.size()); } skey = address.name; From ff2570f0b6cc14c47e06f48ddc45e0a245a20a29 Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 1 Dec 2025 06:11:10 +0000 Subject: [PATCH 04/23] Bind `Context::RemoveDataModel` --- src/bind/Context.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bind/Context.cpp b/src/bind/Context.cpp index 38f2405..63d6423 100644 --- a/src/bind/Context.cpp +++ b/src/bind/Context.cpp @@ -153,6 +153,8 @@ void bind_context(sol::state_view &lua) { &Rml::Context::PushDocumentToBack, "UnfocusDocument", &Rml::Context::UnfocusDocument, + "RemoveDataModel", + &Rml::Context::RemoveDataModel, // RemoveEventListener // G+S From bef80ffa04772288baca91ea653cd5c7ae2403fd Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 1 Dec 2025 20:26:45 +0000 Subject: [PATCH 05/23] Fix discovered issues --- src/bind/DataModel.cpp | 14 ++++++++------ src/plugin/SolLuaDataModel.cpp | 5 +++-- src/plugin/SolLuaDataModel.h | 9 ++++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/bind/DataModel.cpp b/src/bind/DataModel.cpp index eee1347..5b989a8 100644 --- a/src/bind/DataModel.cpp +++ b/src/bind/DataModel.cpp @@ -7,18 +7,20 @@ namespace Rml::SolLua { namespace functions { -static sol::object dataModelGet(SolLuaDataModelTableProxy &self, const std::string &name, sol::this_state s) { - auto proxyTable = self.children.find(name); +static sol::object dataModelGet(SolLuaDataModelTableProxy &self, const sol::object &key, sol::this_state s) { + std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); + auto proxyTable = self.children.find(skey); if (proxyTable != self.children.end()) { return sol::make_object(s, &proxyTable->second); } - return self.objectDef->table().get(name); + return self.objectDef->table().get(key); } static void -dataModelSet(SolLuaDataModelTableProxy &self, const std::string &name, sol::object value, sol::this_state s) { - self.objectDef->table().set(name, value); - self.modelHandle.DirtyVariable(name); +dataModelSet(SolLuaDataModelTableProxy &self, const sol::object &key, sol::object value, sol::this_state s) { + std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); + self.objectDef->table().set(key, value); + self.modelHandle.DirtyVariable(self.topLevelKey ? *self.topLevelKey : skey); } } diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 9f8a9d6..a6b3570 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -61,8 +61,9 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) .objectDef = std::make_unique(value.as()) } ); assert(childProxyIt.second); + childProxyIt.first->second.topLevelKey = proxy.topLevelKey ? proxy.topLevelKey : &childProxyIt.first->first; wrapTable(childProxyIt.first->second, false); - if (skey[0] != '[') { + if (topLevel && skey[0] != '[') { // Skip pseudo-keys - they will be handled in `Child` m_constructor.BindCustomDataVariable( skey, Rml::DataVariable(childProxyIt.first->second.objectDef.get(), &childProxyIt.first->second) @@ -96,7 +97,7 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) ); } else { auto it = proxy.keys.emplace(skey); - if (skey[0] != '[') { + if (topLevel && skey[0] != '[') { // Skip pseudo-keys - they will be handled in `Child` m_constructor.BindCustomDataVariable( skey, Rml::DataVariable(proxy.objectDef.get(), const_cast(it.first->data())) diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index 716ad52..3272db3 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -17,11 +17,14 @@ struct SolLuaDataModelTableProxy { Rml::DataModelHandle modelHandle; std::unique_ptr objectDef; - // Store keys in a set to keep alive the strings - std::unordered_set keys; - // Children proxies for nested tables std::unordered_map children; + + // Store keys of non-table values in a set just to keep alive the strings + std::unordered_set keys; + + // Not string_view to avoid transient copy since Rml expects String& + const std::string *topLevelKey = nullptr; }; class SolLuaDataModel { From a9c9bc2238b9ef087553f5fe714d5380064f7754 Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 1 Dec 2025 20:56:55 +0000 Subject: [PATCH 06/23] Fixes --- src/plugin/SolLuaDataModel.cpp | 16 ++++++++++++---- src/plugin/SolLuaDataModel.h | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index a6b3570..07f28bf 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -64,7 +64,7 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) childProxyIt.first->second.topLevelKey = proxy.topLevelKey ? proxy.topLevelKey : &childProxyIt.first->first; wrapTable(childProxyIt.first->second, false); if (topLevel && skey[0] != '[') { - // Skip pseudo-keys - they will be handled in `Child` + // Only bind top-level non-integer keys m_constructor.BindCustomDataVariable( skey, Rml::DataVariable(childProxyIt.first->second.objectDef.get(), &childProxyIt.first->second) ); @@ -98,7 +98,7 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) } else { auto it = proxy.keys.emplace(skey); if (topLevel && skey[0] != '[') { - // Skip pseudo-keys - they will be handled in `Child` + // Only bind top-level non-integer keys m_constructor.BindCustomDataVariable( skey, Rml::DataVariable(proxy.objectDef.get(), const_cast(it.first->data())) ); @@ -108,6 +108,10 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) } } +void SolLuaDataModel::rebindNestedTable(SolLuaDataModelTableProxy &proxy, const sol::object &key) { + sol::table_proxy valueProxy = proxy.objectDef->table()[key]; +} + /// SolLuaObjectDef SolLuaObjectDef::SolLuaObjectDef(sol::table table) : VariableDefinition(DataVariableType::Scalar), @@ -119,7 +123,9 @@ bool SolLuaObjectDef::Get(void *ptr, Rml::Variant &variant) { auto *key = const_cast(static_cast(ptr)); if (key[0] == '[') { // Pseudo-key: access by index - int idx = std::atoi(key + 1); + int idx; + std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); + assert(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); return false; @@ -154,7 +160,9 @@ bool SolLuaObjectDef::Set(void *ptr, const Rml::Variant &variant) { auto *key = const_cast(static_cast(ptr)); if (key[0] == '[') { // Pseudo-key: access by index - int idx = std::atoi(key + 1); + int idx; + std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); + assert(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); return false; diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index 3272db3..e1cd2ed 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -35,9 +35,9 @@ class SolLuaDataModel { private: void wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel); + void rebindNestedTable(SolLuaDataModelTableProxy &proxy, const sol::object& key); Rml::DataModelConstructor m_constructor; - SolLuaDataModelTableProxy m_topLevelProxy; }; From 2d3e7147abdfda2c9603ed7979e11b11ca8d090b Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 1 Dec 2025 21:34:31 +0000 Subject: [PATCH 07/23] Support rebinding of nested tables when overwritten from lua --- src/bind/DataModel.cpp | 15 ++++++++++---- src/plugin/SolLuaDataModel.cpp | 37 +++++++++++++++++++++++++--------- src/plugin/SolLuaDataModel.h | 13 +++++++++--- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/bind/DataModel.cpp b/src/bind/DataModel.cpp index 5b989a8..806f87a 100644 --- a/src/bind/DataModel.cpp +++ b/src/bind/DataModel.cpp @@ -9,9 +9,9 @@ namespace Rml::SolLua { namespace functions { static sol::object dataModelGet(SolLuaDataModelTableProxy &self, const sol::object &key, sol::this_state s) { std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); - auto proxyTable = self.children.find(skey); - if (proxyTable != self.children.end()) { - return sol::make_object(s, &proxyTable->second); + auto proxyTableIt = self.children.find(skey); + if (proxyTableIt != self.children.end()) { + return sol::make_object(s, &proxyTableIt->second); } return self.objectDef->table().get(key); } @@ -20,7 +20,14 @@ static void dataModelSet(SolLuaDataModelTableProxy &self, const sol::object &key, sol::object value, sol::this_state s) { std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); self.objectDef->table().set(key, value); - self.modelHandle.DirtyVariable(self.topLevelKey ? *self.topLevelKey : skey); + self.model->modelHandle().DirtyVariable(self.topLevelKey ? *self.topLevelKey : skey); + + if (value.get_type() == sol::type::table) { + // Also dirty nested table's proxy + auto proxyTableIt = self.children.find(skey); + assert(proxyTableIt != self.children.end()); + proxyTableIt->second.dirty = true; + } } } diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 07f28bf..4db80ff 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -32,16 +32,29 @@ DataVariable MakeLiteralIntVariable(int value) { SolLuaDataModel::SolLuaDataModel(const sol::table &model, const Rml::DataModelConstructor &constructor) : m_constructor(constructor), - m_topLevelProxy{ .modelHandle = m_constructor.GetModelHandle(), - .objectDef = std::make_unique(model) } { - wrapTable(m_topLevelProxy, true); + m_topLevelProxy{ .model = this, .objectDef = std::make_unique(model) } { + bindTable(m_topLevelProxy, true); +} + +Rml::DataModelHandle SolLuaDataModel::modelHandle() const { + return m_constructor.GetModelHandle(); } SolLuaDataModelTableProxy &SolLuaDataModel::topLevelProxy() { return m_topLevelProxy; } -void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) { +void SolLuaDataModel::rebindNestedTable(SolLuaDataModelTableProxy &nestedProxy, const sol::table &newTable) { + assert(nestedProxy.dirty); + + nestedProxy.children.clear(); // Orphan existing children + nestedProxy.objectDef->setTable(newTable); // Update table + bindTable(nestedProxy, false); // Nested rebind + + nestedProxy.dirty = false; +} + +void SolLuaDataModel::bindTable(SolLuaDataModelTableProxy &proxy, bool topLevel) { for (auto &[key, value] : proxy.objectDef->table()) { std::string skey; if (key.get_type() == sol::type::string) { @@ -52,17 +65,18 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) skey = std::format("[{}]", key.as() - 1); // Lua is 1-based } else { Rml::Log::Message(Log::LT_ERROR, "Data model key with type other than string or integer is unsupported"); + return; } if (value.get_type() == sol::type::table) { auto childProxyIt = proxy.children.emplace( skey, - SolLuaDataModelTableProxy{ .modelHandle = m_topLevelProxy.modelHandle, + SolLuaDataModelTableProxy{ .model = this, .objectDef = std::make_unique(value.as()) } ); assert(childProxyIt.second); childProxyIt.first->second.topLevelKey = proxy.topLevelKey ? proxy.topLevelKey : &childProxyIt.first->first; - wrapTable(childProxyIt.first->second, false); + bindTable(childProxyIt.first->second, false); if (topLevel && skey[0] != '[') { // Only bind top-level non-integer keys m_constructor.BindCustomDataVariable( @@ -108,10 +122,6 @@ void SolLuaDataModel::wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel) } } -void SolLuaDataModel::rebindNestedTable(SolLuaDataModelTableProxy &proxy, const sol::object &key) { - sol::table_proxy valueProxy = proxy.objectDef->table()[key]; -} - /// SolLuaObjectDef SolLuaObjectDef::SolLuaObjectDef(sol::table table) : VariableDefinition(DataVariableType::Scalar), @@ -208,6 +218,9 @@ DataVariable SolLuaObjectDef::Child(void *ptr, const Rml::DataAddressEntry &addr if (obj.get_type() == sol::type::table) { auto it = proxy.children.find(skey); assert(it != proxy.children.end()); + if (it->second.dirty) { + proxy.model->rebindNestedTable(it->second, obj); + } // Pass proxy as ptr to be used in `Child` calls further down the chain return { it->second.objectDef.get(), &it->second }; @@ -222,4 +235,8 @@ sol::table &SolLuaObjectDef::table() { return m_table; } +void SolLuaObjectDef::setTable(sol::table table) { + m_table = std::move(table); +} + } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index e1cd2ed..5e068d0 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -11,10 +11,11 @@ namespace Rml::SolLua { class SolLuaObjectDef; +class SolLuaDataModel; /// Userdata that proxies any table in the model, recursively struct SolLuaDataModelTableProxy { - Rml::DataModelHandle modelHandle; + SolLuaDataModel *model = nullptr; std::unique_ptr objectDef; // Children proxies for nested tables @@ -25,17 +26,21 @@ struct SolLuaDataModelTableProxy { // Not string_view to avoid transient copy since Rml expects String& const std::string *topLevelKey = nullptr; + + bool dirty = false; }; class SolLuaDataModel { public: SolLuaDataModel(const sol::table &model, const Rml::DataModelConstructor &constructor); + Rml::DataModelHandle modelHandle() const; + SolLuaDataModelTableProxy &topLevelProxy(); + void rebindNestedTable(SolLuaDataModelTableProxy &nestedProxy, const sol::table &newTable); private: - void wrapTable(SolLuaDataModelTableProxy &proxy, bool topLevel); - void rebindNestedTable(SolLuaDataModelTableProxy &proxy, const sol::object& key); + void bindTable(SolLuaDataModelTableProxy &proxy, bool topLevel); Rml::DataModelConstructor m_constructor; SolLuaDataModelTableProxy m_topLevelProxy; @@ -51,6 +56,8 @@ class SolLuaObjectDef final : public Rml::VariableDefinition { sol::table &table(); + void setTable(sol::table table); + protected: sol::table m_table; }; From 12a2dc87f94107d9bb99dc33f6a6ddd809106839 Mon Sep 17 00:00:00 2001 From: espkk Date: Wed, 3 Dec 2025 20:10:24 +0000 Subject: [PATCH 08/23] Apply .clang-format --- src/bind/Context.cpp | 357 +++++++++++----------- src/bind/DataModel.cpp | 80 ++--- src/plugin/SolLuaDataModel.cpp | 529 +++++++++++++++++++-------------- src/plugin/SolLuaDataModel.h | 83 +++--- 4 files changed, 572 insertions(+), 477 deletions(-) diff --git a/src/bind/Context.cpp b/src/bind/Context.cpp index 63d6423..8b3e64e 100644 --- a/src/bind/Context.cpp +++ b/src/bind/Context.cpp @@ -2,183 +2,198 @@ #include #include #include -#include -#include #include +#include #include SOLHPP #include "bind.h" -#include "plugin/SolLuaDataModel.h" #include "plugin/SolLuaDocument.h" +#include "plugin/SolLuaDataModel.h" + -namespace Rml::SolLua { - -namespace document { -/// -/// Return a SolLuaDocument. -/// -static auto getDocumentBypass(Rml::Context &self, int idx) { - auto document = self.GetDocument(idx); - auto result = dynamic_cast(document); - return result; -} - -/// -/// Return a SolLuaDocument. -/// -static auto getDocumentBypassString(Rml::Context &self, const Rml::String &name) { - auto document = self.GetDocument(name); - return dynamic_cast(document); -} - -/// -/// Helper function to fill the indexed table with data. -/// -static auto getDocument(Rml::Context &self) { - std::function result = [&self](int idx) -> auto { - return getDocumentBypass(self, idx); - }; - return result; -} -} - -namespace datamodel { - -/// -/// Opens a Lua data model. -/// -/// The context that called this function. -/// The name of the data model. -/// The table to bind as the data model. -/// Lua state. -/// A unique pointer to a Sol Lua Data Model. -static std::shared_ptr -openDataModel(Rml::Context &self, const Rml::String &name, sol::object model, sol::this_state s) { - if (model.get_type() != sol::type::table) { - Rml::Log::Message(Log::LT_ERROR, "Data model must be a table."); - return nullptr; - } - - // Create data model. - auto constructor = self.CreateDataModel(name); - - // Already created? Get existing. - if (!constructor) { - constructor = self.GetDataModel(name); - if (!constructor) { - return nullptr; - } - } - - auto dataModel = std::make_shared(model.as(), constructor); - - // Alias data model to it's top level proxy. - return { dataModel, &dataModel->topLevelProxy() }; -} -} - -namespace element { -static auto getElementAtPoint1(Rml::Context &self, Rml::Vector2f point) { - return self.GetElementAtPoint(point); -} - -static auto getElementAtPoint2(Rml::Context &self, Rml::Vector2f point, Rml::Element &ignore) { - return self.GetElementAtPoint(point, &ignore); -} -} - -/// -/// Binds the Rml::Context class to Lua. -/// -/// The Lua state to bind into. -void bind_context(sol::state_view &lua) { - lua.new_usertype( - "Context", - sol::no_constructor, - // M - "AddEventListener", - &Rml::Context::AddEventListener, - "CreateDocument", - [](Rml::Context &self) { - return self.CreateDocument(); - }, - "LoadDocument", - [](Rml::Context &self, const Rml::String &document) { - auto doc = self.LoadDocument(document); - return dynamic_cast(doc); - }, - "GetDocument", - &document::getDocumentBypassString, - "Render", - &Rml::Context::Render, - "UnloadAllDocuments", - &Rml::Context::UnloadAllDocuments, - "UnloadDocument", - &Rml::Context::UnloadDocument, - "Update", - &Rml::Context::Update, - "OpenDataModel", - &datamodel::openDataModel, - "ProcessMouseMove", - &Rml::Context::ProcessMouseMove, - "ProcessMouseButtonDown", - &Rml::Context::ProcessMouseButtonDown, - "ProcessMouseButtonUp", - &Rml::Context::ProcessMouseButtonUp, - "ProcessMouseWheel", - sol::resolve(&Rml::Context::ProcessMouseWheel), - "ProcessMouseLeave", - &Rml::Context::ProcessMouseLeave, - "IsMouseInteracting", - &Rml::Context::IsMouseInteracting, - "ProcessKeyDown", - &Rml::Context::ProcessKeyDown, - "ProcessKeyUp", - &Rml::Context::ProcessKeyUp, - "ProcessTextInput", - sol::resolve(&Rml::Context::ProcessTextInput), - //-- - "EnableMouseCursor", - &Rml::Context::EnableMouseCursor, - "ActivateTheme", - &Rml::Context::ActivateTheme, - "IsThemeActive", - &Rml::Context::IsThemeActive, - "GetElementAtPoint", - sol::overload(&element::getElementAtPoint1, &element::getElementAtPoint2), - "PullDocumentToFront", - &Rml::Context::PullDocumentToFront, - "PushDocumentToBack", - &Rml::Context::PushDocumentToBack, - "UnfocusDocument", - &Rml::Context::UnfocusDocument, - "RemoveDataModel", - &Rml::Context::RemoveDataModel, - // RemoveEventListener - - // G+S - "dimensions", - sol::property(&Rml::Context::GetDimensions, &Rml::Context::SetDimensions), - "dp_ratio", - sol::property(&Rml::Context::GetDensityIndependentPixelRatio, &Rml::Context::SetDensityIndependentPixelRatio), - - // G - "documents", - sol::readonly_property(&getIndexedTable< - SolLuaDocument, - Rml::Context, - &document::getDocument, - &Rml::Context::GetNumDocuments>), - "focus_element", - sol::readonly_property(&Rml::Context::GetFocusElement), - "hover_element", - sol::readonly_property(&Rml::Context::GetHoverElement), - "name", - sol::readonly_property(&Rml::Context::GetName), - "root_element", - sol::readonly_property(&Rml::Context::GetRootElement) - ); -} +namespace Rml::SolLua +{ + + namespace document + { + /// + /// Return a SolLuaDocument. + /// + static auto getDocumentBypass(Rml::Context& self, int idx) + { + auto document = self.GetDocument(idx); + auto result = dynamic_cast(document); + return result; + } + + /// + /// Return a SolLuaDocument. + /// + static auto getDocumentBypassString(Rml::Context& self, const Rml::String& name) + { + auto document = self.GetDocument(name); + return dynamic_cast(document); + } + + /// + /// Helper function to fill the indexed table with data. + /// + static auto getDocument(Rml::Context& self) + { + std::function result = [&self](int idx) -> auto + { return getDocumentBypass(self, idx); }; + return result; + } + } // namespace document + + namespace datamodel + { + + /// + /// Opens a Lua data model. + /// + /// The context that called this function. + /// The name of the data model. + /// The table to bind as the data model. + /// Lua state. + /// A unique pointer to a Sol Lua Data Model. + static std::shared_ptr + openDataModel(Rml::Context& self, const Rml::String& name, sol::object model, sol::this_state s) + { + if (model.get_type() != sol::type::table) + { + Rml::Log::Message(Log::LT_ERROR, "Data model must be a table."); + return nullptr; + } + + // Create data model. + auto constructor = self.CreateDataModel(name); + + // Already created? Get existing. + if (!constructor) + { + constructor = self.GetDataModel(name); + if (!constructor) + { + return nullptr; + } + } + + auto dataModel = std::make_shared(model.as(), constructor); + + // Alias data model to it's top level proxy. + return {dataModel, &dataModel->topLevelProxy()}; + } + } + + namespace element + { + static auto getElementAtPoint1(Rml::Context& self, Rml::Vector2f point) + { + return self.GetElementAtPoint(point); + } + + static auto getElementAtPoint2(Rml::Context& self, Rml::Vector2f point, Rml::Element& ignore) + { + return self.GetElementAtPoint(point, &ignore); + } + } + + /// + /// Binds the Rml::Context class to Lua. + /// + /// The Lua state to bind into. + void bind_context(sol::state_view& lua) + { + lua.new_usertype( + "Context", + sol::no_constructor, + // M + "AddEventListener", + &Rml::Context::AddEventListener, + "CreateDocument", + [](Rml::Context& self) + { + return self.CreateDocument(); + }, + "LoadDocument", + [](Rml::Context& self, const Rml::String& document) + { + auto doc = self.LoadDocument(document); + return dynamic_cast(doc); + }, + "GetDocument", + &document::getDocumentBypassString, + "Render", + &Rml::Context::Render, + "UnloadAllDocuments", + &Rml::Context::UnloadAllDocuments, + "UnloadDocument", + &Rml::Context::UnloadDocument, + "Update", + &Rml::Context::Update, + "OpenDataModel", + &datamodel::openDataModel, + "ProcessMouseMove", + &Rml::Context::ProcessMouseMove, + "ProcessMouseButtonDown", + &Rml::Context::ProcessMouseButtonDown, + "ProcessMouseButtonUp", + &Rml::Context::ProcessMouseButtonUp, + "ProcessMouseWheel", + sol::resolve(&Rml::Context::ProcessMouseWheel), + "ProcessMouseLeave", + &Rml::Context::ProcessMouseLeave, + "IsMouseInteracting", + &Rml::Context::IsMouseInteracting, + "ProcessKeyDown", + &Rml::Context::ProcessKeyDown, + "ProcessKeyUp", + &Rml::Context::ProcessKeyUp, + "ProcessTextInput", + sol::resolve(&Rml::Context::ProcessTextInput), + //-- + "EnableMouseCursor", + &Rml::Context::EnableMouseCursor, + "ActivateTheme", + &Rml::Context::ActivateTheme, + "IsThemeActive", + &Rml::Context::IsThemeActive, + "GetElementAtPoint", + sol::overload(&element::getElementAtPoint1, &element::getElementAtPoint2), + "PullDocumentToFront", + &Rml::Context::PullDocumentToFront, + "PushDocumentToBack", + &Rml::Context::PushDocumentToBack, + "UnfocusDocument", + &Rml::Context::UnfocusDocument, + "RemoveDataModel", + &Rml::Context::RemoveDataModel, + // RemoveEventListener + + // G+S + "dimensions", + sol::property(&Rml::Context::GetDimensions, &Rml::Context::SetDimensions), + "dp_ratio", + sol::property(&Rml::Context::GetDensityIndependentPixelRatio, &Rml::Context::SetDensityIndependentPixelRatio), + + // G + "documents", + sol::readonly_property(&getIndexedTable< + SolLuaDocument, + Rml::Context, + &document::getDocument, + &Rml::Context::GetNumDocuments>), + "focus_element", + sol::readonly_property(&Rml::Context::GetFocusElement), + "hover_element", + sol::readonly_property(&Rml::Context::GetHoverElement), + "name", + sol::readonly_property(&Rml::Context::GetName), + "root_element", + sol::readonly_property(&Rml::Context::GetRootElement) + ); + } } // end namespace Rml::SolLua diff --git a/src/bind/DataModel.cpp b/src/bind/DataModel.cpp index 806f87a..1819623 100644 --- a/src/bind/DataModel.cpp +++ b/src/bind/DataModel.cpp @@ -5,41 +5,49 @@ #include "plugin/SolLuaDataModel.h" -namespace Rml::SolLua { -namespace functions { -static sol::object dataModelGet(SolLuaDataModelTableProxy &self, const sol::object &key, sol::this_state s) { - std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); - auto proxyTableIt = self.children.find(skey); - if (proxyTableIt != self.children.end()) { - return sol::make_object(s, &proxyTableIt->second); - } - return self.objectDef->table().get(key); -} - -static void -dataModelSet(SolLuaDataModelTableProxy &self, const sol::object &key, sol::object value, sol::this_state s) { - std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); - self.objectDef->table().set(key, value); - self.model->modelHandle().DirtyVariable(self.topLevelKey ? *self.topLevelKey : skey); - - if (value.get_type() == sol::type::table) { - // Also dirty nested table's proxy - auto proxyTableIt = self.children.find(skey); - assert(proxyTableIt != self.children.end()); - proxyTableIt->second.dirty = true; - } -} -} - -void bind_datamodel(sol::state_view &lua) { - lua.new_usertype( - "SolLuaDataModelTableProxy", - sol::no_constructor, - sol::meta_function::index, - &functions::dataModelGet, - sol::meta_function::new_index, - &functions::dataModelSet - ); -} + +namespace Rml::SolLua +{ + namespace functions + { + static sol::object dataModelGet(SolLuaDataModelTableProxy& self, const sol::object& key, sol::this_state s) + { + std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); + auto proxyTableIt = self.children.find(skey); + if (proxyTableIt != self.children.end()) + { + return sol::make_object(s, &proxyTableIt->second); + } + return self.objectDef->table().get(key); + } + + static void + dataModelSet(SolLuaDataModelTableProxy& self, const sol::object& key, sol::object value, sol::this_state s) + { + std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); + self.objectDef->table().set(key, value); + self.model->modelHandle().DirtyVariable(self.topLevelKey ? *self.topLevelKey : skey); + + if (value.get_type() == sol::type::table) + { + // Also dirty nested table's proxy + auto proxyTableIt = self.children.find(skey); + assert(proxyTableIt != self.children.end()); + proxyTableIt->second.dirty = true; + } + } + } + + void bind_datamodel(sol::state_view& lua) + { + lua.new_usertype( + "SolLuaDataModelTableProxy", + sol::no_constructor, + sol::meta_function::index, + &functions::dataModelGet, + sol::meta_function::new_index, + &functions::dataModelSet + ); + } } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 4db80ff..3fc2f0c 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -1,242 +1,309 @@ #include -#include #include +#include #include SOLHPP #include "SolLuaDataModel.h" + #include "SolLuaDocument.h" #include "bind/bind.h" -namespace Rml::SolLua { -namespace { - -class LiteralIntDefinition final : public VariableDefinition { - public: - LiteralIntDefinition() - : VariableDefinition(DataVariableType::Scalar) { - } - - bool Get(void *ptr, Variant &variant) override { - variant = static_cast(reinterpret_cast(ptr)); - return true; - } -}; - -DataVariable MakeLiteralIntVariable(int value) { - static LiteralIntDefinition literal_int_definition; - return DataVariable(&literal_int_definition, reinterpret_cast(static_cast(value))); -} - -} - -SolLuaDataModel::SolLuaDataModel(const sol::table &model, const Rml::DataModelConstructor &constructor) - : m_constructor(constructor), - m_topLevelProxy{ .model = this, .objectDef = std::make_unique(model) } { - bindTable(m_topLevelProxy, true); -} - -Rml::DataModelHandle SolLuaDataModel::modelHandle() const { - return m_constructor.GetModelHandle(); -} - -SolLuaDataModelTableProxy &SolLuaDataModel::topLevelProxy() { - return m_topLevelProxy; -} - -void SolLuaDataModel::rebindNestedTable(SolLuaDataModelTableProxy &nestedProxy, const sol::table &newTable) { - assert(nestedProxy.dirty); - - nestedProxy.children.clear(); // Orphan existing children - nestedProxy.objectDef->setTable(newTable); // Update table - bindTable(nestedProxy, false); // Nested rebind - - nestedProxy.dirty = false; -} - -void SolLuaDataModel::bindTable(SolLuaDataModelTableProxy &proxy, bool topLevel) { - for (auto &[key, value] : proxy.objectDef->table()) { - std::string skey; - if (key.get_type() == sol::type::string) { - skey = key.as(); - } else if (key.get_type() == sol::type::number) { - // Assign a pseudo-key for numeric indices - // TODO: check if the number is an integer? - skey = std::format("[{}]", key.as() - 1); // Lua is 1-based - } else { - Rml::Log::Message(Log::LT_ERROR, "Data model key with type other than string or integer is unsupported"); - return; - } - - if (value.get_type() == sol::type::table) { - auto childProxyIt = proxy.children.emplace( - skey, - SolLuaDataModelTableProxy{ .model = this, - .objectDef = std::make_unique(value.as()) } - ); - assert(childProxyIt.second); - childProxyIt.first->second.topLevelKey = proxy.topLevelKey ? proxy.topLevelKey : &childProxyIt.first->first; - bindTable(childProxyIt.first->second, false); - if (topLevel && skey[0] != '[') { - // Only bind top-level non-integer keys - m_constructor.BindCustomDataVariable( - skey, Rml::DataVariable(childProxyIt.first->second.objectDef.get(), &childProxyIt.first->second) - ); - } - } else { - if (value.get_type() == sol::type::function) { - if (!topLevel) { - Rml::Log::Message( - Log::LT_WARNING, "Event callbacks are only allowed at the top level of a data model." - ); - continue; - } - - m_constructor.BindEventCallback( - skey, - [cb = sol::protected_function{ value }, - state = sol::state_view{ proxy.objectDef->table().lua_state( - ) }](Rml::DataModelHandle, Rml::Event &event, const Rml::VariantList &varlist) { - if (cb.valid()) { - std::vector args; - for (const auto &variant : varlist) { - args.push_back(makeObjectFromVariant(&variant, state)); - } - auto pfr = cb(event, sol::as_args(args)); - if (!pfr.valid()) { - ErrorHandler(cb.lua_state(), std::move(pfr)); - } - } - } - ); - } else { - auto it = proxy.keys.emplace(skey); - if (topLevel && skey[0] != '[') { - // Only bind top-level non-integer keys - m_constructor.BindCustomDataVariable( - skey, Rml::DataVariable(proxy.objectDef.get(), const_cast(it.first->data())) - ); - } - } - } - } -} - -/// SolLuaObjectDef -SolLuaObjectDef::SolLuaObjectDef(sol::table table) - : VariableDefinition(DataVariableType::Scalar), - m_table(std::move(table)) { -} - -bool SolLuaObjectDef::Get(void *ptr, Rml::Variant &variant) { - sol::object obj; - auto *key = const_cast(static_cast(ptr)); - if (key[0] == '[') { - // Pseudo-key: access by index - int idx; - std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); - assert(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); - if (idx < 0 || idx >= m_table.size()) { - Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); - return false; - } - obj = m_table[idx + 1]; // Lua is 1-based - } else { - obj = m_table[key]; - } - - if (obj.is()) { - variant = obj.as(); - } else if (obj.is()) { - variant = obj.as(); - } else if (obj.is()) { - variant = obj.as(); - } else if (obj.is()) { - variant = obj.as(); - } else if (obj.is()) { - variant = obj.as(); - } else if (obj.is()) { - variant = obj.as(); - } else if (obj.is()) { - variant = obj.as(); - } else { - variant = Rml::Variant{}; - } - - return true; -} - -bool SolLuaObjectDef::Set(void *ptr, const Rml::Variant &variant) { - auto *key = const_cast(static_cast(ptr)); - if (key[0] == '[') { - // Pseudo-key: access by index - int idx; - std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); - assert(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); - if (idx < 0 || idx >= m_table.size()) { - Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); - return false; - } - sol::table_proxy obj = m_table[idx + 1]; // Lua is 1-based - obj = makeObjectFromVariant(&variant, m_table.lua_state()); - } else { - sol::table_proxy obj = m_table[key]; - obj = makeObjectFromVariant(&variant, m_table.lua_state()); - } - return true; -} - -int SolLuaObjectDef::Size(void *ptr) { - // TODO: can size be called on non-proxy objects? - SolLuaDataModelTableProxy &proxy = *static_cast(ptr); - return static_cast(proxy.objectDef->table().size()); -} - -DataVariable SolLuaObjectDef::Child(void *ptr, const Rml::DataAddressEntry &address) { - SolLuaDataModelTableProxy &proxy = *static_cast(ptr); - sol::table t = proxy.objectDef->table(); - - std::string skey; - sol::object obj; - if (address.index != -1) { - if (address.index < 0 || address.index >= t.size()) { - Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); - return {}; - } - // Access by index - skey = std::format("[{}]", address.index); - obj = t[address.index + 1]; // Lua is 1-based - } else { - if (address.name == "size") { - return MakeLiteralIntVariable(t.size()); - } - - skey = address.name; - obj = proxy.objectDef->table()[address.name]; - } - - if (obj.get_type() == sol::type::table) { - auto it = proxy.children.find(skey); - assert(it != proxy.children.end()); - if (it->second.dirty) { - proxy.model->rebindNestedTable(it->second, obj); - } - - // Pass proxy as ptr to be used in `Child` calls further down the chain - return { it->second.objectDef.get(), &it->second }; - } - - auto it = proxy.keys.find(skey); - assert(it != proxy.keys.end()); - return { proxy.objectDef.get(), const_cast(it->data()) }; -} - -sol::table &SolLuaObjectDef::table() { - return m_table; -} - -void SolLuaObjectDef::setTable(sol::table table) { - m_table = std::move(table); -} +namespace Rml::SolLua +{ + namespace + { + + class LiteralIntDefinition final : public VariableDefinition + { + public: + LiteralIntDefinition() + : VariableDefinition(DataVariableType::Scalar) + { + } + + bool Get(void* ptr, Variant& variant) override + { + variant = static_cast(reinterpret_cast(ptr)); + return true; + } + }; + + DataVariable MakeLiteralIntVariable(int value) + { + static LiteralIntDefinition literal_int_definition; + return DataVariable(&literal_int_definition, reinterpret_cast(static_cast(value))); + } + + } // namespace + + SolLuaDataModel::SolLuaDataModel(const sol::table& model, const Rml::DataModelConstructor& constructor) + : m_constructor(constructor), + m_topLevelProxy{.model = this, .objectDef = std::make_unique(model)} + { + bindTable(m_topLevelProxy, true); + } + + Rml::DataModelHandle SolLuaDataModel::modelHandle() const + { + return m_constructor.GetModelHandle(); + } + + SolLuaDataModelTableProxy& SolLuaDataModel::topLevelProxy() + { + return m_topLevelProxy; + } + + void SolLuaDataModel::rebindNestedTable(SolLuaDataModelTableProxy& nestedProxy, const sol::table& newTable) + { + assert(nestedProxy.dirty); + + nestedProxy.children.clear(); // Orphan existing children + nestedProxy.objectDef->setTable(newTable); // Update table + bindTable(nestedProxy, false); // Nested rebind + + nestedProxy.dirty = false; + } + + void SolLuaDataModel::bindTable(SolLuaDataModelTableProxy& proxy, bool topLevel) + { + for (auto& [key, value] : proxy.objectDef->table()) + { + std::string skey; + if (key.get_type() == sol::type::string) + { + skey = key.as(); + } + else if (key.get_type() == sol::type::number) + { + // Assign a pseudo-key for numeric indices + // TODO: check if the number is an integer? + skey = std::format("[{}]", key.as() - 1); // Lua is 1-based + } + else + { + Rml::Log::Message(Log::LT_ERROR, "Data model key with type other than string or integer is unsupported"); + return; + } + + if (value.get_type() == sol::type::table) + { + auto childProxyIt = proxy.children.emplace( + skey, + SolLuaDataModelTableProxy{.model = this, .objectDef = std::make_unique(value.as())} + ); + assert(childProxyIt.second); + childProxyIt.first->second.topLevelKey = proxy.topLevelKey ? proxy.topLevelKey : &childProxyIt.first->first; + bindTable(childProxyIt.first->second, false); + if (topLevel && skey[0] != '[') + { + // Only bind top-level non-integer keys + m_constructor.BindCustomDataVariable( + skey, Rml::DataVariable(childProxyIt.first->second.objectDef.get(), &childProxyIt.first->second) + ); + } + } + else + { + if (value.get_type() == sol::type::function) + { + if (!topLevel) + { + Rml::Log::Message( + Log::LT_WARNING, "Event callbacks are only allowed at the top level of a data model." + ); + continue; + } + + m_constructor.BindEventCallback( + skey, + [cb = sol::protected_function{value}, + state = sol::state_view{proxy.objectDef->table().lua_state( + )}](Rml::DataModelHandle, Rml::Event& event, const Rml::VariantList& varlist) + { + if (cb.valid()) + { + std::vector args; + for (const auto& variant : varlist) + { + args.push_back(makeObjectFromVariant(&variant, state)); + } + auto pfr = cb(event, sol::as_args(args)); + if (!pfr.valid()) + { + ErrorHandler(cb.lua_state(), std::move(pfr)); + } + } + } + ); + } + else + { + auto it = proxy.keys.emplace(skey); + if (topLevel && skey[0] != '[') + { + // Only bind top-level non-integer keys + m_constructor.BindCustomDataVariable( + skey, Rml::DataVariable(proxy.objectDef.get(), const_cast(it.first->data())) + ); + } + } + } + } + } + + /// SolLuaObjectDef + SolLuaObjectDef::SolLuaObjectDef(sol::table table) + : VariableDefinition(DataVariableType::Scalar), + m_table(std::move(table)) + { + } + + bool SolLuaObjectDef::Get(void* ptr, Rml::Variant& variant) + { + sol::object obj; + auto* key = const_cast(static_cast(ptr)); + if (key[0] == '[') + { + // Pseudo-key: access by index + int idx; + std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); + assert(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); + if (idx < 0 || idx >= m_table.size()) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); + return false; + } + obj = m_table[idx + 1]; // Lua is 1-based + } + else + { + obj = m_table[key]; + } + + if (obj.is()) + { + variant = obj.as(); + } + else if (obj.is()) + { + variant = obj.as(); + } + else if (obj.is()) + { + variant = obj.as(); + } + else if (obj.is()) + { + variant = obj.as(); + } + else if (obj.is()) + { + variant = obj.as(); + } + else if (obj.is()) + { + variant = obj.as(); + } + else if (obj.is()) + { + variant = obj.as(); + } + else + { + variant = Rml::Variant{}; + } + + return true; + } + + bool SolLuaObjectDef::Set(void* ptr, const Rml::Variant& variant) + { + auto* key = const_cast(static_cast(ptr)); + if (key[0] == '[') + { + // Pseudo-key: access by index + int idx; + std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); + assert(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); + if (idx < 0 || idx >= m_table.size()) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); + return false; + } + sol::table_proxy obj = m_table[idx + 1]; // Lua is 1-based + obj = makeObjectFromVariant(&variant, m_table.lua_state()); + } + else + { + sol::table_proxy obj = m_table[key]; + obj = makeObjectFromVariant(&variant, m_table.lua_state()); + } + return true; + } + + int SolLuaObjectDef::Size(void* ptr) + { + // TODO: can size be called on non-proxy objects? + SolLuaDataModelTableProxy& proxy = *static_cast(ptr); + return static_cast(proxy.objectDef->table().size()); + } + + DataVariable SolLuaObjectDef::Child(void* ptr, const Rml::DataAddressEntry& address) + { + SolLuaDataModelTableProxy& proxy = *static_cast(ptr); + sol::table t = proxy.objectDef->table(); + + std::string skey; + sol::object obj; + if (address.index != -1) + { + if (address.index < 0 || address.index >= t.size()) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); + return {}; + } + // Access by index + skey = std::format("[{}]", address.index); + obj = t[address.index + 1]; // Lua is 1-based + } + else + { + if (address.name == "size") + { + return MakeLiteralIntVariable(t.size()); + } + + skey = address.name; + obj = proxy.objectDef->table()[address.name]; + } + + if (obj.get_type() == sol::type::table) + { + auto it = proxy.children.find(skey); + assert(it != proxy.children.end()); + if (it->second.dirty) + { + proxy.model->rebindNestedTable(it->second, obj); + } + + // Pass proxy as ptr to be used in `Child` calls further down the chain + return {it->second.objectDef.get(), &it->second}; + } + + auto it = proxy.keys.find(skey); + assert(it != proxy.keys.end()); + return {proxy.objectDef.get(), const_cast(it->data())}; + } + + sol::table& SolLuaObjectDef::table() + { + return m_table; + } + + void SolLuaObjectDef::setTable(sol::table table) + { + m_table = std::move(table); + } } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index 5e068d0..b89572e 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -5,61 +5,66 @@ #include #include -#include #include +#include #include SOLHPP -namespace Rml::SolLua { -class SolLuaObjectDef; -class SolLuaDataModel; -/// Userdata that proxies any table in the model, recursively -struct SolLuaDataModelTableProxy { - SolLuaDataModel *model = nullptr; - std::unique_ptr objectDef; +namespace Rml::SolLua +{ + class SolLuaObjectDef; + class SolLuaDataModel; + + /// Userdata that proxies any table in the model, recursively + struct SolLuaDataModelTableProxy + { + SolLuaDataModel* model = nullptr; + std::unique_ptr objectDef; - // Children proxies for nested tables - std::unordered_map children; + // Children proxies for nested tables + std::unordered_map children; - // Store keys of non-table values in a set just to keep alive the strings - std::unordered_set keys; + // Store keys of non-table values in a set just to keep alive the strings + std::unordered_set keys; - // Not string_view to avoid transient copy since Rml expects String& - const std::string *topLevelKey = nullptr; + // Not string_view to avoid transient copy since Rml expects String& + const std::string* topLevelKey = nullptr; - bool dirty = false; -}; + bool dirty = false; + }; -class SolLuaDataModel { - public: - SolLuaDataModel(const sol::table &model, const Rml::DataModelConstructor &constructor); + class SolLuaDataModel + { + public: + SolLuaDataModel(const sol::table& model, const Rml::DataModelConstructor& constructor); - Rml::DataModelHandle modelHandle() const; + Rml::DataModelHandle modelHandle() const; - SolLuaDataModelTableProxy &topLevelProxy(); - void rebindNestedTable(SolLuaDataModelTableProxy &nestedProxy, const sol::table &newTable); + SolLuaDataModelTableProxy& topLevelProxy(); + void rebindNestedTable(SolLuaDataModelTableProxy& nestedProxy, const sol::table& newTable); - private: - void bindTable(SolLuaDataModelTableProxy &proxy, bool topLevel); + private: + void bindTable(SolLuaDataModelTableProxy& proxy, bool topLevel); - Rml::DataModelConstructor m_constructor; - SolLuaDataModelTableProxy m_topLevelProxy; -}; + Rml::DataModelConstructor m_constructor; + SolLuaDataModelTableProxy m_topLevelProxy; + }; -class SolLuaObjectDef final : public Rml::VariableDefinition { - public: - SolLuaObjectDef(sol::table table); - bool Get(void *ptr, Rml::Variant &variant) override; - bool Set(void *ptr, const Rml::Variant &variant) override; - int Size(void *ptr) override; - DataVariable Child(void *ptr, const Rml::DataAddressEntry &address) override; + class SolLuaObjectDef final : public Rml::VariableDefinition + { + public: + SolLuaObjectDef(sol::table table); + bool Get(void* ptr, Rml::Variant& variant) override; + bool Set(void* ptr, const Rml::Variant& variant) override; + int Size(void* ptr) override; + DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override; - sol::table &table(); + sol::table& table(); - void setTable(sol::table table); + void setTable(sol::table table); - protected: - sol::table m_table; -}; + protected: + sol::table m_table; + }; } // end namespace Rml::SolLua From b5e6d5b0f265870f4b892d0b6acc95cbf2bc7ab1 Mon Sep 17 00:00:00 2001 From: espkk Date: Fri, 5 Dec 2025 01:45:47 +0000 Subject: [PATCH 09/23] Simplify implementation --- src/bind/Context.cpp | 27 +--- src/bind/DataModel.cpp | 29 +--- src/plugin/SolLuaDataModel.cpp | 277 +++++++++++++++++---------------- src/plugin/SolLuaDataModel.h | 57 +++---- 4 files changed, 181 insertions(+), 209 deletions(-) diff --git a/src/bind/Context.cpp b/src/bind/Context.cpp index b5f5f66..2829d6c 100644 --- a/src/bind/Context.cpp +++ b/src/bind/Context.cpp @@ -57,7 +57,7 @@ namespace Rml::SolLua /// The table to bind as the data model. /// Lua state. /// A unique pointer to a Sol Lua Data Model. - static std::shared_ptr + static std::shared_ptr openDataModel(Rml::Context& self, const Rml::String& name, sol::object model, sol::this_state s) { if (model.get_type() != sol::type::table) @@ -142,26 +142,15 @@ namespace Rml::SolLua // RemoveEventListener // G+S - "dimensions", - sol::property(&Rml::Context::GetDimensions, &Rml::Context::SetDimensions), - "dp_ratio", - sol::property(&Rml::Context::GetDensityIndependentPixelRatio, &Rml::Context::SetDensityIndependentPixelRatio), + "dimensions", sol::property(&Rml::Context::GetDimensions, &Rml::Context::SetDimensions), + "dp_ratio", sol::property(&Rml::Context::GetDensityIndependentPixelRatio, &Rml::Context::SetDensityIndependentPixelRatio), // G - "documents", - sol::readonly_property(&getIndexedTable< - SolLuaDocument, - Rml::Context, - &document::getDocument, - &Rml::Context::GetNumDocuments>), - "focus_element", - sol::readonly_property(&Rml::Context::GetFocusElement), - "hover_element", - sol::readonly_property(&Rml::Context::GetHoverElement), - "name", - sol::readonly_property(&Rml::Context::GetName), - "root_element", - sol::readonly_property(&Rml::Context::GetRootElement) + "documents", sol::readonly_property(&getIndexedTable), + "focus_element", sol::readonly_property(&Rml::Context::GetFocusElement), + "hover_element", sol::readonly_property(&Rml::Context::GetHoverElement), + "name", sol::readonly_property(&Rml::Context::GetName), + "root_element", sol::readonly_property(&Rml::Context::GetRootElement) ); // clang-format on } diff --git a/src/bind/DataModel.cpp b/src/bind/DataModel.cpp index f3b3477..5ad3b12 100644 --- a/src/bind/DataModel.cpp +++ b/src/bind/DataModel.cpp @@ -1,5 +1,3 @@ -#include - #include #include SOLHPP @@ -9,42 +7,25 @@ namespace Rml::SolLua { namespace functions { - static sol::object dataModelGet(SolLuaDataModelTableProxy& self, const sol::object& key, sol::this_state s) + static sol::object dataModelGet(SolLuaDataModelProxy& self, const sol::object& key) { - std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); - auto proxyTableIt = self.children.find(skey); - if (proxyTableIt != self.children.end()) - { - return sol::make_object(s, &proxyTableIt->second); - } - return self.objectDef->table().get(key); + return self.get(key); } static void - dataModelSet(SolLuaDataModelTableProxy& self, const sol::object& key, sol::object value, sol::this_state s) + dataModelSet(SolLuaDataModelProxy& self, const sol::object& key, sol::object value) { - std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); - self.objectDef->table().set(key, value); - self.model->modelHandle().DirtyVariable(self.topLevelKey ? *self.topLevelKey : skey); - - if (value.get_type() == sol::type::table) - { - // Also dirty nested table's proxy - auto proxyTableIt = self.children.find(skey); - assert(proxyTableIt != self.children.end()); - proxyTableIt->second.dirty = true; - } + self.set(key, value); } } // namespace functions void bind_datamodel(sol::state_view& lua) { // clang-format off - lua.new_usertype("SolLuaDataModelTableProxy", sol::no_constructor, + lua.new_usertype("SolLuaDataModelProxy", sol::no_constructor, sol::meta_function::index, &functions::dataModelGet, sol::meta_function::new_index, &functions::dataModelSet ); // clang-format on } - } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 41b8744..b929cb6 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -39,127 +39,29 @@ namespace Rml::SolLua SolLuaDataModel::SolLuaDataModel(const sol::table& model, const Rml::DataModelConstructor& constructor) : m_constructor(constructor), - m_topLevelProxy{.model = this, .objectDef = std::make_unique(model)} + m_topLevelProxy(this, model) { - bindTable(m_topLevelProxy, true); + m_topLevelProxy.bind(true); } - Rml::DataModelHandle SolLuaDataModel::modelHandle() const + Rml::DataModelConstructor SolLuaDataModel::constructor() const { - return m_constructor.GetModelHandle(); + return m_constructor; } - SolLuaDataModelTableProxy& SolLuaDataModel::topLevelProxy() + SolLuaDataModelProxy& SolLuaDataModel::topLevelProxy() { return m_topLevelProxy; } - void SolLuaDataModel::rebindNestedTable(SolLuaDataModelTableProxy& nestedProxy, const sol::table& newTable) - { - assert(nestedProxy.dirty); - - nestedProxy.children.clear(); // Orphan existing children - nestedProxy.objectDef->setTable(newTable); // Update table - bindTable(nestedProxy, false); // Nested rebind - - nestedProxy.dirty = false; - } - - void SolLuaDataModel::bindTable(SolLuaDataModelTableProxy& proxy, bool topLevel) - { - for (auto& [key, value] : proxy.objectDef->table()) - { - std::string skey; - if (key.get_type() == sol::type::string) - { - skey = key.as(); - } - else if (key.get_type() == sol::type::number) - { - // Assign a pseudo-key for numeric indices - // TODO: check if the number is an integer? - skey = std::format("[{}]", key.as() - 1); // Lua is 1-based - } - else - { - Rml::Log::Message(Log::LT_ERROR, "Data model key with type other than string or integer is unsupported"); - return; - } - - if (value.get_type() == sol::type::table) - { - auto childProxyIt = proxy.children.emplace( - skey, - SolLuaDataModelTableProxy{.model = this, .objectDef = std::make_unique(value.as())} - ); - assert(childProxyIt.second); - childProxyIt.first->second.topLevelKey = proxy.topLevelKey ? proxy.topLevelKey : &childProxyIt.first->first; - bindTable(childProxyIt.first->second, false); - if (topLevel && skey[0] != '[') - { - // Only bind top-level non-integer keys - m_constructor.BindCustomDataVariable( - skey, Rml::DataVariable(childProxyIt.first->second.objectDef.get(), &childProxyIt.first->second) - ); - } - } - else - { - if (value.get_type() == sol::type::function) - { - if (!topLevel) - { - Rml::Log::Message( - Log::LT_WARNING, "Event callbacks are only allowed at the top level of a data model." - ); - continue; - } - - m_constructor.BindEventCallback( - skey, - [cb = sol::protected_function{value}, - state = sol::state_view{proxy.objectDef->table().lua_state( - )}](Rml::DataModelHandle, Rml::Event& event, const Rml::VariantList& varlist) - { - if (cb.valid()) - { - std::vector args; - for (const auto& variant : varlist) - { - args.push_back(makeObjectFromVariant(&variant, state)); - } - auto pfr = cb(event, sol::as_args(args)); - if (!pfr.valid()) - { - ErrorHandler(cb.lua_state(), std::move(pfr)); - } - } - } - ); - } - else - { - auto it = proxy.keys.emplace(skey); - if (topLevel && skey[0] != '[') - { - // Only bind top-level non-integer keys - m_constructor.BindCustomDataVariable( - skey, Rml::DataVariable(proxy.objectDef.get(), const_cast(it.first->data())) - ); - } - } - } - } - } - - /// SolLuaObjectDef - SolLuaObjectDef::SolLuaObjectDef(sol::table table) + /// SolLuaDatamodelProxy + SolLuaDataModelProxy::SolLuaDataModelProxy(SolLuaDataModel* datamodel, sol::table table) : VariableDefinition(DataVariableType::Scalar), - m_table(std::move(table)) + m_datamodel(datamodel), m_table(std::move(table)) { } - bool SolLuaObjectDef::Get(void* ptr, Rml::Variant& variant) + bool SolLuaDataModelProxy::Get(void* ptr, Rml::Variant& variant) { sol::object obj; auto* key = const_cast(static_cast(ptr)); @@ -168,7 +70,7 @@ namespace Rml::SolLua // Pseudo-key: access by index int idx; std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); - assert(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); + RMLUI_ASSERT(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); @@ -217,7 +119,7 @@ namespace Rml::SolLua return true; } - bool SolLuaObjectDef::Set(void* ptr, const Rml::Variant& variant) + bool SolLuaDataModelProxy::Set(void* ptr, const Rml::Variant& variant) { auto* key = const_cast(static_cast(ptr)); if (key[0] == '[') @@ -225,7 +127,7 @@ namespace Rml::SolLua // Pseudo-key: access by index int idx; std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); - assert(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); + RMLUI_ASSERT(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); @@ -242,69 +144,178 @@ namespace Rml::SolLua return true; } - int SolLuaObjectDef::Size(void* ptr) + int SolLuaDataModelProxy::Size(void* ptr) { - // TODO: can size be called on non-proxy objects? - SolLuaDataModelTableProxy& proxy = *static_cast(ptr); - return static_cast(proxy.objectDef->table().size()); + SolLuaDataModelProxy& proxy = *static_cast(ptr); + return static_cast(proxy.m_table.size()); } - DataVariable SolLuaObjectDef::Child(void* ptr, const Rml::DataAddressEntry& address) + DataVariable SolLuaDataModelProxy::Child(void* ptr, const Rml::DataAddressEntry& address) { - SolLuaDataModelTableProxy& proxy = *static_cast(ptr); - sol::table t = proxy.objectDef->table(); + SolLuaDataModelProxy& proxy = *static_cast(ptr); std::string skey; sol::object obj; if (address.index != -1) { - if (address.index < 0 || address.index >= t.size()) + if (address.index < 0 || address.index >= m_table.size()) { Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); return {}; } // Access by index skey = std::format("[{}]", address.index); - obj = t[address.index + 1]; // Lua is 1-based + obj = m_table[address.index + 1]; // Lua is 1-based } else { if (address.name == "size") { - return MakeLiteralIntVariable(t.size()); + return MakeLiteralIntVariable(m_table.size()); } skey = address.name; - obj = proxy.objectDef->table()[address.name]; + obj = proxy.m_table[address.name]; } if (obj.get_type() == sol::type::table) { - auto it = proxy.children.find(skey); - assert(it != proxy.children.end()); - if (it->second.dirty) + auto it = proxy.m_children.find(skey); + RMLUI_ASSERT(it != proxy.m_children.end()); + if (it->second.m_dirty) { - proxy.model->rebindNestedTable(it->second, obj); + it->second.rebind(obj); } // Pass proxy as ptr to be used in `Child` calls further down the chain - return {it->second.objectDef.get(), &it->second}; + return {&it->second, &it->second}; } - auto it = proxy.keys.find(skey); - assert(it != proxy.keys.end()); - return {proxy.objectDef.get(), const_cast(it->data())}; + auto it = proxy.m_keys.find(skey); + RMLUI_ASSERT(it != proxy.m_keys.end()); + return {&proxy, const_cast(it->data())}; } - sol::table& SolLuaObjectDef::table() + sol::object SolLuaDataModelProxy::get(const sol::object& key) const { - return m_table; + std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); + auto proxyIt = m_children.find(skey); + if (proxyIt != m_children.end()) + { + return sol::make_object_userdata(m_table.lua_state(), &proxyIt->second); + } + return m_table.get(key); } - void SolLuaObjectDef::setTable(sol::table table) + void SolLuaDataModelProxy::set(const sol::object& key, sol::object value) { - m_table = std::move(table); + std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); + m_table.set(key, value); + m_datamodel->constructor().GetModelHandle().DirtyVariable(m_topLevelKey ? *m_topLevelKey : skey); + + if (value.get_type() == sol::type::table) + { + // Also dirty nested table's proxy + auto proxyTableIt = m_children.find(skey); + RMLUI_ASSERT(proxyTableIt != m_children.end()); + proxyTableIt->second.m_dirty = true; + } + } + + void SolLuaDataModelProxy::bind(bool topLevel) + { + for (auto& [key, value] : m_table) + { + std::string skey; + if (key.get_type() == sol::type::string) + { + skey = key.as(); + } + else if (key.get_type() == sol::type::number) + { + // Assign a pseudo-key for numeric indices + // TODO: check if the number is an integer? + skey = std::format("[{}]", key.as() - 1); // Lua is 1-based + } + else + { + Rml::Log::Message(Log::LT_ERROR, "Data model key with type other than string or integer is unsupported"); + return; + } + + if (value.get_type() == sol::type::table) + { + auto childProxyIt = m_children.try_emplace(skey, m_datamodel, value.as()); + RMLUI_ASSERT(childProxyIt.second); + childProxyIt.first->second.m_topLevelKey = m_topLevelKey ? m_topLevelKey : &childProxyIt.first->first; + childProxyIt.first->second.bind(false); + if (topLevel && skey[0] != '[') + { + // Only bind top-level non-integer keys + m_datamodel->constructor().BindCustomDataVariable( + skey, Rml::DataVariable(&childProxyIt.first->second, &childProxyIt.first->second) + ); + } + } + else + { + if (value.get_type() == sol::type::function) + { + if (!topLevel) + { + Rml::Log::Message( + Log::LT_WARNING, "Event callbacks are only allowed at the top level of a data model." + ); + continue; + } + + m_datamodel->constructor().BindEventCallback( + skey, + [cb = sol::protected_function{value}, + state = sol::state_view{m_table.lua_state( + )}](Rml::DataModelHandle, Rml::Event& event, const Rml::VariantList& varlist) + { + if (cb.valid()) + { + std::vector args; + for (const auto& variant : varlist) + { + args.push_back(makeObjectFromVariant(&variant, state)); + } + auto pfr = cb(event, sol::as_args(args)); + if (!pfr.valid()) + { + ErrorHandler(cb.lua_state(), std::move(pfr)); + } + } + } + ); + } + else + { + auto it = m_keys.emplace(skey); + if (topLevel && skey[0] != '[') + { + // Only bind top-level non-integer keys + m_datamodel->constructor().BindCustomDataVariable( + skey, Rml::DataVariable(this, const_cast(it.first->data())) + ); + } + } + } + } + } + + void SolLuaDataModelProxy::rebind(const sol::table& newTable) + { + RMLUI_ASSERT(m_dirty); + + m_children.clear(); // Orphan existing children + m_table = newTable; // Update table + bind(false); // Nested rebind + + m_dirty = false; } } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index 7b74cc5..9f95bca 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -11,25 +11,37 @@ namespace Rml::SolLua { - class SolLuaObjectDef; class SolLuaDataModel; - /// Userdata that proxies any table in the model, recursively - struct SolLuaDataModelTableProxy + class SolLuaDataModelProxy final : public Rml::VariableDefinition { - SolLuaDataModel* model = nullptr; - std::unique_ptr objectDef; + public: + SolLuaDataModelProxy(SolLuaDataModel* datamodel, sol::table table); + bool Get(void* ptr, Rml::Variant& variant) override; + bool Set(void* ptr, const Rml::Variant& variant) override; + int Size(void* ptr) override; + DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override; + + sol::object get(const sol::object& key) const; + void set(const sol::object& key, sol::object value); + + void bind(bool topLevel); + void rebind(const sol::table& newTable); + + protected: + SolLuaDataModel* m_datamodel; + sol::table m_table; // Children proxies for nested tables - std::unordered_map children; + std::unordered_map m_children; // Store keys of non-table values in a set just to keep alive the strings - std::unordered_set keys; + std::unordered_set m_keys; // Not string_view to avoid transient copy since Rml expects String& - const std::string* topLevelKey = nullptr; + const std::string* m_topLevelKey = nullptr; - bool dirty = false; + bool m_dirty = false; }; class SolLuaDataModel @@ -37,33 +49,12 @@ namespace Rml::SolLua public: SolLuaDataModel(const sol::table& model, const Rml::DataModelConstructor& constructor); - Rml::DataModelHandle modelHandle() const; - - SolLuaDataModelTableProxy& topLevelProxy(); - void rebindNestedTable(SolLuaDataModelTableProxy& nestedProxy, const sol::table& newTable); + Rml::DataModelConstructor constructor() const; + SolLuaDataModelProxy& topLevelProxy(); private: - void bindTable(SolLuaDataModelTableProxy& proxy, bool topLevel); - Rml::DataModelConstructor m_constructor; - SolLuaDataModelTableProxy m_topLevelProxy; - }; - - class SolLuaObjectDef final : public Rml::VariableDefinition - { - public: - SolLuaObjectDef(sol::table table); - bool Get(void* ptr, Rml::Variant& variant) override; - bool Set(void* ptr, const Rml::Variant& variant) override; - int Size(void* ptr) override; - DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override; - - sol::table& table(); - - void setTable(sol::table table); - - protected: - sol::table m_table; + SolLuaDataModelProxy m_topLevelProxy; }; } // end namespace Rml::SolLua From 1286c9f7011b3193c4ea0e6866aff581ec9b94f9 Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 8 Dec 2025 03:25:11 +0000 Subject: [PATCH 10/23] Don't force find_package if target is present --- CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a41fcbe..6e3f385 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,11 +79,15 @@ if (DEFINED SOL_INCLUDE_DIR) endif() # RmlUI -find_package(RmlUi CONFIG REQUIRED) +if(NOT TARGET RmlUi::Core) + find_package(RmlUi CONFIG REQUIRED) +endif() target_link_libraries(RmlSolLua PUBLIC RmlUi::Core) # Sol2 -find_package(sol2 CONFIG REQUIRED) +if(NOT TARGET sol2) + find_package(sol2 CONFIG REQUIRED) +endif() target_link_libraries(RmlSolLua PUBLIC sol2) # Try to find a Lua. From f7550b3f68369208bcdbbf95c42a2720e249db41 Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 8 Dec 2025 17:36:25 +0000 Subject: [PATCH 11/23] Apply nested rebind and improve sanitization --- src/plugin/SolLuaDataModel.cpp | 30 ++++++++++++------------------ src/plugin/SolLuaDataModel.h | 2 -- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index b929cb6..dc03039 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -183,12 +183,6 @@ namespace Rml::SolLua { auto it = proxy.m_children.find(skey); RMLUI_ASSERT(it != proxy.m_children.end()); - if (it->second.m_dirty) - { - it->second.rebind(obj); - } - - // Pass proxy as ptr to be used in `Child` calls further down the chain return {&it->second, &it->second}; } @@ -216,10 +210,10 @@ namespace Rml::SolLua if (value.get_type() == sol::type::table) { - // Also dirty nested table's proxy + // Rebind nested table's proxy auto proxyTableIt = m_children.find(skey); RMLUI_ASSERT(proxyTableIt != m_children.end()); - proxyTableIt->second.m_dirty = true; + proxyTableIt->second.rebind(value); } } @@ -234,14 +228,18 @@ namespace Rml::SolLua } else if (key.get_type() == sol::type::number) { - // Assign a pseudo-key for numeric indices - // TODO: check if the number is an integer? - skey = std::format("[{}]", key.as() - 1); // Lua is 1-based + const double number = key.as() - 1; + const bool isInteger = !std::isfinite(number) && std::floor(number) == number; + if (isInteger && number >= 0) + { + // Assign a pseudo-key for numeric indices + // Assuming it fits into uint64_t to simplify logic + skey = std::format("[{}]", static_cast(number)); // Lua is 1-based + } } - else + if (skey.empty()) { - Rml::Log::Message(Log::LT_ERROR, "Data model key with type other than string or integer is unsupported"); - return; + Rml::Log::Message(Log::LT_ERROR, "Data model key other than non-empty string or non-negative integer is unsupported"); } if (value.get_type() == sol::type::table) @@ -309,13 +307,9 @@ namespace Rml::SolLua void SolLuaDataModelProxy::rebind(const sol::table& newTable) { - RMLUI_ASSERT(m_dirty); - m_children.clear(); // Orphan existing children m_table = newTable; // Update table bind(false); // Nested rebind - - m_dirty = false; } } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index 9f95bca..efcb97b 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -40,8 +40,6 @@ namespace Rml::SolLua // Not string_view to avoid transient copy since Rml expects String& const std::string* m_topLevelKey = nullptr; - - bool m_dirty = false; }; class SolLuaDataModel From 3a8b3679baeb035608ecf62f23f9d5cfb216abd5 Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 8 Dec 2025 18:26:52 +0000 Subject: [PATCH 12/23] Add more sanitization and make it more consistent --- src/plugin/SolLuaDataModel.cpp | 82 +++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index dc03039..9e1ccf9 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -31,8 +31,29 @@ namespace Rml::SolLua DataVariable MakeLiteralIntVariable(int value) { - static LiteralIntDefinition literal_int_definition; - return DataVariable(&literal_int_definition, reinterpret_cast(static_cast(value))); + static LiteralIntDefinition def; + return DataVariable(&def, reinterpret_cast(static_cast(value))); + } + + class NullDefinition final : public VariableDefinition + { + public: + NullDefinition() + : VariableDefinition(DataVariableType::Scalar) + { + } + + bool Get(void*, Variant& variant) override + { + variant.Clear(); + return true; + } + }; + + DataVariable MakeNullVariable() + { + static NullDefinition def; + return DataVariable(&def, nullptr); } } // namespace @@ -63,6 +84,13 @@ namespace Rml::SolLua bool SolLuaDataModelProxy::Get(void* ptr, Rml::Variant& variant) { + if (ptr == nullptr) + { + // TODO: alternatively, the table can be serialized to a string and returned + Rml::Log::Message(Rml::Log::LT_ERROR, "[LUA][ERROR] Trying to access a table as a scalar from VariableDefinition::Get"); + return false; + } + sol::object obj; auto* key = const_cast(static_cast(ptr)); if (key[0] == '[') @@ -73,7 +101,7 @@ namespace Rml::SolLua RMLUI_ASSERT(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { - Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); + Rml::Log::Message(Rml::Log::LT_ERROR, "[LUA][ERROR] Data array index out of bounds"); return false; } obj = m_table[idx + 1]; // Lua is 1-based @@ -121,6 +149,12 @@ namespace Rml::SolLua bool SolLuaDataModelProxy::Set(void* ptr, const Rml::Variant& variant) { + if (ptr == nullptr) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "[LUA][ERROR] Trying to access a table as a scalar from VariableDefinition::Set"); + return false; + } + auto* key = const_cast(static_cast(ptr)); if (key[0] == '[') { @@ -130,7 +164,7 @@ namespace Rml::SolLua RMLUI_ASSERT(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { - Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); + Rml::Log::Message(Rml::Log::LT_ERROR, "[LUA][ERROR] Data array index out of bounds"); return false; } sol::table_proxy obj = m_table[idx + 1]; // Lua is 1-based @@ -146,22 +180,29 @@ namespace Rml::SolLua int SolLuaDataModelProxy::Size(void* ptr) { - SolLuaDataModelProxy& proxy = *static_cast(ptr); - return static_cast(proxy.m_table.size()); + if (ptr != nullptr) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "[LUA][ERROR] Trying to get size of a scalar"); + return 0; + } + return static_cast(m_table.size()); } DataVariable SolLuaDataModelProxy::Child(void* ptr, const Rml::DataAddressEntry& address) { - SolLuaDataModelProxy& proxy = *static_cast(ptr); + if (ptr != nullptr) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "[LUA][ERROR] Trying to access a sub element of a scalar"); + return {}; + } std::string skey; sol::object obj; if (address.index != -1) { - if (address.index < 0 || address.index >= m_table.size()) { - Rml::Log::Message(Rml::Log::LT_ERROR, "Data array index out of bounds."); + Rml::Log::Message(Rml::Log::LT_ERROR, "[LUA][ERROR] Data array index out of bounds"); return {}; } // Access by index @@ -176,19 +217,19 @@ namespace Rml::SolLua } skey = address.name; - obj = proxy.m_table[address.name]; + obj = m_table[address.name]; } if (obj.get_type() == sol::type::table) { - auto it = proxy.m_children.find(skey); - RMLUI_ASSERT(it != proxy.m_children.end()); - return {&it->second, &it->second}; + auto it = m_children.find(skey); + RMLUI_ASSERT(it != m_children.end()); + return {&it->second, nullptr}; } - auto it = proxy.m_keys.find(skey); - RMLUI_ASSERT(it != proxy.m_keys.end()); - return {&proxy, const_cast(it->data())}; + auto it = m_keys.find(skey); + RMLUI_ASSERT(it != m_keys.end()); + return {this, const_cast(it->data())}; } sol::object SolLuaDataModelProxy::get(const sol::object& key) const @@ -229,7 +270,7 @@ namespace Rml::SolLua else if (key.get_type() == sol::type::number) { const double number = key.as() - 1; - const bool isInteger = !std::isfinite(number) && std::floor(number) == number; + const bool isInteger = std::isfinite(number) && std::floor(number) == number; if (isInteger && number >= 0) { // Assign a pseudo-key for numeric indices @@ -239,7 +280,8 @@ namespace Rml::SolLua } if (skey.empty()) { - Rml::Log::Message(Log::LT_ERROR, "Data model key other than non-empty string or non-negative integer is unsupported"); + Rml::Log::Message(Log::LT_ERROR, "[LUA][ERROR] Data model key other than non-empty string or non-negative integer is unsupported"); + return; } if (value.get_type() == sol::type::table) @@ -252,7 +294,7 @@ namespace Rml::SolLua { // Only bind top-level non-integer keys m_datamodel->constructor().BindCustomDataVariable( - skey, Rml::DataVariable(&childProxyIt.first->second, &childProxyIt.first->second) + skey, Rml::DataVariable(&childProxyIt.first->second, nullptr) ); } } @@ -263,7 +305,7 @@ namespace Rml::SolLua if (!topLevel) { Rml::Log::Message( - Log::LT_WARNING, "Event callbacks are only allowed at the top level of a data model." + Log::LT_WARNING, "[LUA][WARNING] Event callbacks are only allowed at the top level of a data model" ); continue; } From 888cd86cc963a56e5809ee01419d22439b60b7b4 Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 8 Dec 2025 21:54:01 +0000 Subject: [PATCH 13/23] Remove unused code --- src/plugin/SolLuaDataModel.cpp | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 9e1ccf9..c84ab81 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -35,27 +35,6 @@ namespace Rml::SolLua return DataVariable(&def, reinterpret_cast(static_cast(value))); } - class NullDefinition final : public VariableDefinition - { - public: - NullDefinition() - : VariableDefinition(DataVariableType::Scalar) - { - } - - bool Get(void*, Variant& variant) override - { - variant.Clear(); - return true; - } - }; - - DataVariable MakeNullVariable() - { - static NullDefinition def; - return DataVariable(&def, nullptr); - } - } // namespace SolLuaDataModel::SolLuaDataModel(const sol::table& model, const Rml::DataModelConstructor& constructor) From d0ba2c9f6480c80bb36076a5bb384c330148fe80 Mon Sep 17 00:00:00 2001 From: espkk Date: Sat, 13 Dec 2025 04:21:58 +0000 Subject: [PATCH 14/23] Don't crash when nested key is not available and support late binding --- src/plugin/SolLuaDataModel.cpp | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index c84ab81..78fd513 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -202,12 +202,18 @@ namespace Rml::SolLua if (obj.get_type() == sol::type::table) { auto it = m_children.find(skey); + // The assumption is that this can only happen if the table was there + // at the moment of rebind, hence it has to be in `m_children`. RMLUI_ASSERT(it != m_children.end()); return {&it->second, nullptr}; } auto it = m_keys.find(skey); - RMLUI_ASSERT(it != m_keys.end()); + if (it == m_keys.end()) + { + // Key is not in the proxy + return {}; + } return {this, const_cast(it->data())}; } @@ -226,7 +232,6 @@ namespace Rml::SolLua { std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); m_table.set(key, value); - m_datamodel->constructor().GetModelHandle().DirtyVariable(m_topLevelKey ? *m_topLevelKey : skey); if (value.get_type() == sol::type::table) { @@ -235,6 +240,21 @@ namespace Rml::SolLua RMLUI_ASSERT(proxyTableIt != m_children.end()); proxyTableIt->second.rebind(value); } + else + { + if (!m_keys.contains(skey)) + { + // Late binding - new key has been just added (only for nested tables) + // Technically, we could do the same+`BindCustomDataVariable` for a top-level table as well, + // but it seems to be unsupported by RmlUi itself as it continues to assume that it doesn't exist + if (m_topLevelKey != nullptr) + { + m_keys.emplace(skey); + } + } + } + + m_datamodel->constructor().GetModelHandle().DirtyVariable(m_topLevelKey ? *m_topLevelKey : skey); } void SolLuaDataModelProxy::bind(bool topLevel) @@ -292,8 +312,7 @@ namespace Rml::SolLua m_datamodel->constructor().BindEventCallback( skey, [cb = sol::protected_function{value}, - state = sol::state_view{m_table.lua_state( - )}](Rml::DataModelHandle, Rml::Event& event, const Rml::VariantList& varlist) + state = sol::state_view{m_table.lua_state()}](Rml::DataModelHandle, Rml::Event& event, const Rml::VariantList& varlist) { if (cb.valid()) { From 0f00c5b38351b19e5f26ec199daba595f268de5f Mon Sep 17 00:00:00 2001 From: John Norman Date: Wed, 31 Dec 2025 13:42:09 -0800 Subject: [PATCH 15/23] Fixed compile errors and made some small edits. --- src/bind/DataModel.cpp | 3 +-- src/plugin/SolLuaDataModel.cpp | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/bind/DataModel.cpp b/src/bind/DataModel.cpp index 5ad3b12..a3e99a1 100644 --- a/src/bind/DataModel.cpp +++ b/src/bind/DataModel.cpp @@ -12,8 +12,7 @@ namespace Rml::SolLua return self.get(key); } - static void - dataModelSet(SolLuaDataModelProxy& self, const sol::object& key, sol::object value) + static void dataModelSet(SolLuaDataModelProxy& self, const sol::object& key, sol::object value) { self.set(key, value); } diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 78fd513..ace072e 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -1,8 +1,18 @@ +#include +#include +#include +#include +#include #include +#include +#include +#include #include #include +#include #include SOLHPP +#include #include "SolLuaDataModel.h" @@ -71,12 +81,12 @@ namespace Rml::SolLua } sol::object obj; - auto* key = const_cast(static_cast(ptr)); - if (key[0] == '[') + std::string_view key{ static_cast(ptr) }; + if (key.length() > 2 && key[0] == '[') { // Pseudo-key: access by index int idx; - std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); + std::from_chars_result result = std::from_chars(key.data() + 1, key.data() + key.length() - 1, idx); RMLUI_ASSERT(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { @@ -134,12 +144,12 @@ namespace Rml::SolLua return false; } - auto* key = const_cast(static_cast(ptr)); - if (key[0] == '[') + std::string_view key{ static_cast(ptr) }; + if (key.length() > 2 && key[0] == '[') { // Pseudo-key: access by index int idx; - std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); + std::from_chars_result result = std::from_chars(key.data() + 1, key.data() + key.length() - 1, idx); RMLUI_ASSERT(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { From a2da6d430d09d2c46e55e858558a812698b48f31 Mon Sep 17 00:00:00 2001 From: espkk Date: Sun, 1 Mar 2026 22:13:18 +0000 Subject: [PATCH 16/23] Partial revert (restore O(1)) --- src/plugin/SolLuaDataModel.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index ace072e..4b98d43 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -81,12 +81,12 @@ namespace Rml::SolLua } sol::object obj; - std::string_view key{ static_cast(ptr) }; - if (key.length() > 2 && key[0] == '[') + auto* key = const_cast(static_cast(ptr)); + if (key[0] == '[') { // Pseudo-key: access by index int idx; - std::from_chars_result result = std::from_chars(key.data() + 1, key.data() + key.length() - 1, idx); + std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); RMLUI_ASSERT(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { @@ -144,12 +144,12 @@ namespace Rml::SolLua return false; } - std::string_view key{ static_cast(ptr) }; - if (key.length() > 2 && key[0] == '[') + auto* key = const_cast(static_cast(ptr)); + if (key[0] == '[') { // Pseudo-key: access by index int idx; - std::from_chars_result result = std::from_chars(key.data() + 1, key.data() + key.length() - 1, idx); + std::from_chars_result result = std::from_chars(key + 1, key + std::strlen(key) - 1, idx); RMLUI_ASSERT(result.ec == std::errc{} && "Rml failed to sanitize user input to be well-formed"); if (idx < 0 || idx >= m_table.size()) { From 1fe25ee1813b0b58a189df1c91f9a45c308658e6 Mon Sep 17 00:00:00 2001 From: espkk Date: Sun, 1 Mar 2026 22:17:46 +0000 Subject: [PATCH 17/23] Support `__len` metamethod --- src/bind/DataModel.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/bind/DataModel.cpp b/src/bind/DataModel.cpp index a3e99a1..a4385f3 100644 --- a/src/bind/DataModel.cpp +++ b/src/bind/DataModel.cpp @@ -16,6 +16,11 @@ namespace Rml::SolLua { self.set(key, value); } + + static sol::object dataModelLength(SolLuaDataModelProxy& self, sol::this_state state) + { + return sol::make_object(state, self.Size(nullptr)); + } } // namespace functions void bind_datamodel(sol::state_view& lua) @@ -23,7 +28,8 @@ namespace Rml::SolLua // clang-format off lua.new_usertype("SolLuaDataModelProxy", sol::no_constructor, sol::meta_function::index, &functions::dataModelGet, - sol::meta_function::new_index, &functions::dataModelSet + sol::meta_function::new_index, &functions::dataModelSet, + sol::meta_function::length, &functions::dataModelLength ); // clang-format on } From 593741b24c9b979799aac4ab98dce247a0cbaa55 Mon Sep 17 00:00:00 2001 From: espkk Date: Sun, 1 Mar 2026 22:18:16 +0000 Subject: [PATCH 18/23] Fix inserting new array elements --- src/plugin/SolLuaDataModel.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 4b98d43..ac66f8f 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -245,10 +245,21 @@ namespace Rml::SolLua if (value.get_type() == sol::type::table) { - // Rebind nested table's proxy - auto proxyTableIt = m_children.find(skey); - RMLUI_ASSERT(proxyTableIt != m_children.end()); - proxyTableIt->second.rebind(value); + const auto proxyTableIt = m_children.find(skey); + if (proxyTableIt == m_children.end() && skey[0] == '[' && m_topLevelKey != nullptr) + { + // New array element + auto childProxyIt = m_children.try_emplace(skey, m_datamodel, value.as()); + RMLUI_ASSERT(childProxyIt.second); + childProxyIt.first->second.m_topLevelKey = m_topLevelKey; + childProxyIt.first->second.bind(false); + } + else + { + // Existing element - rebind nested table's proxy + RMLUI_ASSERT(proxyTableIt != m_children.end()); + proxyTableIt->second.rebind(value); + } } else { From eca313aab9772f667da087a4de7be2d7a81fc38d Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 2 Mar 2026 21:42:55 +0000 Subject: [PATCH 19/23] Add reflection support for RmlDebugger --- src/plugin/SolLuaDataModel.cpp | 52 ++++++++++++++++++++++++++++++---- src/plugin/SolLuaDataModel.h | 5 +++- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index ac66f8f..0ffa68c 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -2,8 +2,9 @@ #include #include #include -#include +#include #include +#include #include #include #include @@ -23,6 +24,22 @@ namespace Rml::SolLua { namespace { + // Proxy definition to return self as a scalar definition (for non-table keys) + class ScalarDefinitionProxy final : public VariableDefinition + { + public: + ScalarDefinitionProxy(SolLuaDataModelProxy* self) + : VariableDefinition(DataVariableType::Scalar), m_self(self) + { + } + + bool Get(void* ptr, Variant& variant) override + { + return m_self->Get(ptr, variant); + } + + SolLuaDataModelProxy* m_self; + }; class LiteralIntDefinition final : public VariableDefinition { @@ -66,8 +83,8 @@ namespace Rml::SolLua /// SolLuaDatamodelProxy SolLuaDataModelProxy::SolLuaDataModelProxy(SolLuaDataModel* datamodel, sol::table table) - : VariableDefinition(DataVariableType::Scalar), - m_datamodel(datamodel), m_table(std::move(table)) + : VariableDefinition(DataVariableType::Struct), + m_datamodel(datamodel), m_table(std::move(table)), m_selfAsScalar(std::make_unique(this)) { } @@ -189,6 +206,7 @@ namespace Rml::SolLua sol::object obj; if (address.index != -1) { + // Table treated as array (e.g. data-for and co) if (address.index < 0 || address.index >= m_table.size()) { Rml::Log::Message(Rml::Log::LT_ERROR, "[LUA][ERROR] Data array index out of bounds"); @@ -198,6 +216,21 @@ namespace Rml::SolLua skey = std::format("[{}]", address.index); obj = m_table[address.index + 1]; // Lua is 1-based } + else if (address.name.starts_with('[')) + { + // Table treated as struct (e.g. via reflection) + RMLUI_ASSERT(address.name.ends_with(']')); + RMLUI_ASSERT(address.name.size() > 2); + + const std::string_view indexStr(address.name.data() + 1, address.name.size() - 2); + std::int32_t index = -1; + [[maybe_unused]] auto [end, ec] = std::from_chars(indexStr.data(), indexStr.data() + indexStr.size(), index); + RMLUI_ASSERT(ec == std::errc{}); + RMLUI_ASSERT(end == indexStr.data() + indexStr.size()); + + skey = address.name; + obj = m_table[index + 1]; // Lua is 1-based + } else { if (address.name == "size") @@ -224,7 +257,16 @@ namespace Rml::SolLua // Key is not in the proxy return {}; } - return {this, const_cast(it->data())}; + return {m_selfAsScalar.get(), const_cast(it->data())}; + } + + StringList SolLuaDataModelProxy::ReflectMemberNames() + { + StringList names; + names.reserve(m_keys.size() + m_children.size()); + names.append_range(m_keys); + names.append_range(m_children | std::views::keys); + return names; } sol::object SolLuaDataModelProxy::get(const sol::object& key) const @@ -358,7 +400,7 @@ namespace Rml::SolLua { // Only bind top-level non-integer keys m_datamodel->constructor().BindCustomDataVariable( - skey, Rml::DataVariable(this, const_cast(it.first->data())) + skey, Rml::DataVariable(m_selfAsScalar.get(), const_cast(it.first->data())) ); } } diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index efcb97b..0071bd0 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -21,6 +21,7 @@ namespace Rml::SolLua bool Set(void* ptr, const Rml::Variant& variant) override; int Size(void* ptr) override; DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override; + StringList ReflectMemberNames() override; sol::object get(const sol::object& key) const; void set(const sol::object& key, sol::object value); @@ -28,10 +29,12 @@ namespace Rml::SolLua void bind(bool topLevel); void rebind(const sol::table& newTable); - protected: + private: SolLuaDataModel* m_datamodel; sol::table m_table; + std::unique_ptr m_selfAsScalar; + // Children proxies for nested tables std::unordered_map m_children; From 431b8c7b2a935b4b569aad984de03e8900c2d7d0 Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 2 Mar 2026 22:21:29 +0000 Subject: [PATCH 20/23] `IsArrayIndex` --- src/plugin/SolLuaDataModel.cpp | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 0ffa68c..c2fe9f3 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -62,6 +62,17 @@ namespace Rml::SolLua return DataVariable(&def, reinterpret_cast(static_cast(value))); } + bool IsArrayIndex(std::string_view key) + { + return key.starts_with('['); + } + + // @pre Key is a valid null-terminated string + bool IsArrayIndex(const char* key) + { + return key[0] == '['; + } + } // namespace SolLuaDataModel::SolLuaDataModel(const sol::table& model, const Rml::DataModelConstructor& constructor) @@ -99,7 +110,7 @@ namespace Rml::SolLua sol::object obj; auto* key = const_cast(static_cast(ptr)); - if (key[0] == '[') + if (IsArrayIndex(key)) { // Pseudo-key: access by index int idx; @@ -162,7 +173,7 @@ namespace Rml::SolLua } auto* key = const_cast(static_cast(ptr)); - if (key[0] == '[') + if (IsArrayIndex(key)) { // Pseudo-key: access by index int idx; @@ -216,7 +227,7 @@ namespace Rml::SolLua skey = std::format("[{}]", address.index); obj = m_table[address.index + 1]; // Lua is 1-based } - else if (address.name.starts_with('[')) + else if (IsArrayIndex(address.name)) { // Table treated as struct (e.g. via reflection) RMLUI_ASSERT(address.name.ends_with(']')); @@ -288,7 +299,7 @@ namespace Rml::SolLua if (value.get_type() == sol::type::table) { const auto proxyTableIt = m_children.find(skey); - if (proxyTableIt == m_children.end() && skey[0] == '[' && m_topLevelKey != nullptr) + if (proxyTableIt == m_children.end() && IsArrayIndex(skey) && m_topLevelKey != nullptr) { // New array element auto childProxyIt = m_children.try_emplace(skey, m_datamodel, value.as()); @@ -352,7 +363,7 @@ namespace Rml::SolLua RMLUI_ASSERT(childProxyIt.second); childProxyIt.first->second.m_topLevelKey = m_topLevelKey ? m_topLevelKey : &childProxyIt.first->first; childProxyIt.first->second.bind(false); - if (topLevel && skey[0] != '[') + if (topLevel && !IsArrayIndex(skey)) { // Only bind top-level non-integer keys m_datamodel->constructor().BindCustomDataVariable( @@ -396,7 +407,7 @@ namespace Rml::SolLua else { auto it = m_keys.emplace(skey); - if (topLevel && skey[0] != '[') + if (topLevel && !IsArrayIndex(skey)) { // Only bind top-level non-integer keys m_datamodel->constructor().BindCustomDataVariable( From a4c9dbe4ee7d246fed1681f3775e3f3c36948f7f Mon Sep 17 00:00:00 2001 From: espkk Date: Mon, 16 Mar 2026 20:01:15 +0000 Subject: [PATCH 21/23] Fix setting scalars in datamodel from RmlUI --- src/plugin/SolLuaDataModel.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index c2fe9f3..28266d6 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -38,6 +38,11 @@ namespace Rml::SolLua return m_self->Get(ptr, variant); } + bool Set(void* ptr, const Variant& variant) override + { + return m_self->Set(ptr, variant); + } + SolLuaDataModelProxy* m_self; }; From 4d7451cec0da7b7e5e9a793b49c84129964db6fc Mon Sep 17 00:00:00 2001 From: espkk Date: Thu, 26 Mar 2026 00:30:41 +0000 Subject: [PATCH 22/23] - Add `pairs` and `ipairs` metamethods - Optimize hotpath usecases --- src/bind/Context.cpp | 16 +++++-- src/bind/DataModel.cpp | 32 +++++++++---- src/plugin/SolLuaDataModel.cpp | 88 +++++++++++++++++++++++++++------- src/plugin/SolLuaDataModel.h | 18 ++++++- 4 files changed, 120 insertions(+), 34 deletions(-) diff --git a/src/bind/Context.cpp b/src/bind/Context.cpp index 2829d6c..027848d 100644 --- a/src/bind/Context.cpp +++ b/src/bind/Context.cpp @@ -57,13 +57,13 @@ namespace Rml::SolLua /// The table to bind as the data model. /// Lua state. /// A unique pointer to a Sol Lua Data Model. - static std::shared_ptr + static sol::object openDataModel(Rml::Context& self, const Rml::String& name, sol::object model, sol::this_state s) { if (model.get_type() != sol::type::table) { Rml::Log::Message(Log::LT_ERROR, "Data model must be a table."); - return nullptr; + return sol::make_object(s, sol::lua_nil); } // Create data model. @@ -75,14 +75,20 @@ namespace Rml::SolLua constructor = self.GetDataModel(name); if (!constructor) { - return nullptr; + return sol::make_object(s, sol::lua_nil); } } auto dataModel = std::make_shared(model.as(), constructor); - // Alias data model to it's top level proxy. - return {dataModel, &dataModel->topLevelProxy()}; + // Alias data model to its top level proxy and push as shared_ptr userdata. + lua_State* L = s; + sol::object obj = sol::make_object(L, std::shared_ptr(dataModel, &dataModel->topLevelProxy())); + obj.push(L); + dataModel->topLevelProxy().table().push(L); + lua_setuservalue(L, -2); + lua_pop(L, 1); + return obj; } } // namespace datamodel diff --git a/src/bind/DataModel.cpp b/src/bind/DataModel.cpp index a4385f3..e294da2 100644 --- a/src/bind/DataModel.cpp +++ b/src/bind/DataModel.cpp @@ -5,21 +5,33 @@ namespace Rml::SolLua { + /// The following functions use uservalue/fenv to query the underlying table, + /// rather than accessing the proxy itself, to reduce indirection level and improve cache locality. namespace functions { - static sol::object dataModelGet(SolLuaDataModelProxy& self, const sol::object& key) + static int dataModelPairs(lua_State* L) // [-0, +3] { - return self.get(key); + lua_getuservalue(L, 1); // [tbl] + lua_getglobal(L, "next"); // [tbl, next] + lua_pushvalue(L, -2); // [tbl, next, tbl] + lua_pushnil(L); // [tbl, next, tbl, nil] + return 3; } - static void dataModelSet(SolLuaDataModelProxy& self, const sol::object& key, sol::object value) + static int dataModelIpairs(lua_State* L) // [-0, +3] { - self.set(key, value); + lua_getuservalue(L, 1); // [tbl] + lua_getglobal(L, "ipairs"); // [tbl, ipairs] + lua_pushvalue(L, -2); // [tbl, ipairs, tbl] + lua_call(L, 1, 3); // [tbl, iter, tbl, 0] + return 3; } - static sol::object dataModelLength(SolLuaDataModelProxy& self, sol::this_state state) + static int dataModelLen(lua_State* L) // [-0, +1] { - return sol::make_object(state, self.Size(nullptr)); + lua_getuservalue(L, 1); // [tbl] + lua_pushinteger(L, static_cast(lua_rawlen(L, -1))); // [tbl, len] + return 1; } } // namespace functions @@ -27,9 +39,11 @@ namespace Rml::SolLua { // clang-format off lua.new_usertype("SolLuaDataModelProxy", sol::no_constructor, - sol::meta_function::index, &functions::dataModelGet, - sol::meta_function::new_index, &functions::dataModelSet, - sol::meta_function::length, &functions::dataModelLength + sol::meta_function::index, &SolLuaDataModelProxy::luaIndex, + sol::meta_function::new_index, &SolLuaDataModelProxy::luaNewIndex, + sol::meta_function::length, &functions::dataModelLen, + sol::meta_function::pairs, &functions::dataModelPairs, + sol::meta_function::ipairs, &functions::dataModelIpairs ); // clang-format on } diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 28266d6..04d5714 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -285,55 +285,98 @@ namespace Rml::SolLua return names; } - sol::object SolLuaDataModelProxy::get(const sol::object& key) const + void SolLuaDataModelProxy::cacheUserdata() { + lua_State* L = m_table.lua_state(); + m_luaUserdata = sol::make_object_userdata(L, this); + + // Attach the backing Lua table as the userdata's uservalue + { + m_luaUserdata.push(L); // [ud] + m_table.push(L); // [ud, tbl] + lua_setuservalue(L, -2); // [ud] + lua_pop(L, 1); // [] + } + } + + sol::object SolLuaDataModelProxy::luaIndex(SolLuaDataModelProxy& self, sol::stack_object key, sol::this_state ts) + { + lua_State* L = ts; std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); - auto proxyIt = m_children.find(skey); - if (proxyIt != m_children.end()) + + auto it = self.m_children.find(skey); + if (it != self.m_children.end()) + { + if (!it->second.m_luaUserdata.valid()) + it->second.cacheUserdata(); + return it->second.m_luaUserdata; + } + + // Raw lookup in the uservalue table (the proxy's backing Lua table) { - return sol::make_object_userdata(m_table.lua_state(), &proxyIt->second); + lua_getuservalue(L, 1); // [tbl] + key.push(L); // [tbl, key] + lua_rawget(L, -2); // [tbl, value] + sol::object result(L, -1); + lua_pop(L, 2); // [] + return result; } - return m_table.get(key); } - void SolLuaDataModelProxy::set(const sol::object& key, sol::object value) + void SolLuaDataModelProxy::luaNewIndex(SolLuaDataModelProxy& self, sol::stack_object key, sol::stack_object value, sol::this_state ts) { + lua_State* L = ts; std::string skey = key.is() ? key.as() : std::format("[{}]", key.as() - 1); - m_table.set(key, value); + + // Raw store into the uservalue table (the proxy's backing Lua table) + { + lua_getuservalue(L, 1); // [tbl] + key.push(L); // [tbl, key] + value.push(L); // [tbl, key, value] + lua_rawset(L, -3); // [tbl] + lua_pop(L, 1); // [] + } if (value.get_type() == sol::type::table) { - const auto proxyTableIt = m_children.find(skey); - if (proxyTableIt == m_children.end() && IsArrayIndex(skey) && m_topLevelKey != nullptr) + const auto proxyTableIt = self.m_children.find(skey); + if (proxyTableIt == self.m_children.end() && IsArrayIndex(skey) && self.m_topLevelKey != nullptr) { // New array element - auto childProxyIt = m_children.try_emplace(skey, m_datamodel, value.as()); + auto childProxyIt = self.m_children.try_emplace(skey, self.m_datamodel, value.as()); RMLUI_ASSERT(childProxyIt.second); - childProxyIt.first->second.m_topLevelKey = m_topLevelKey; + childProxyIt.first->second.m_topLevelKey = self.m_topLevelKey; childProxyIt.first->second.bind(false); } else { + if (proxyTableIt == self.m_children.end()) + { + Rml::Log::Message( + Log::LT_ERROR, "[LUA][ERROR] Adding new named table to the datamodel is not supported, add it as an array element instead" + ); + return; + } + // Existing element - rebind nested table's proxy - RMLUI_ASSERT(proxyTableIt != m_children.end()); - proxyTableIt->second.rebind(value); + proxyTableIt->second.rebind(value.as()); } } else { - if (!m_keys.contains(skey)) + if (!self.m_keys.contains(skey)) { // Late binding - new key has been just added (only for nested tables) // Technically, we could do the same+`BindCustomDataVariable` for a top-level table as well, // but it seems to be unsupported by RmlUi itself as it continues to assume that it doesn't exist - if (m_topLevelKey != nullptr) + if (self.m_topLevelKey != nullptr) { - m_keys.emplace(skey); + self.m_keys.emplace(skey); } } } - m_datamodel->constructor().GetModelHandle().DirtyVariable(m_topLevelKey ? *m_topLevelKey : skey); + self.m_datamodel->constructor().GetModelHandle().DirtyVariable(self.m_topLevelKey ? *self.m_topLevelKey : skey); } void SolLuaDataModelProxy::bind(bool topLevel) @@ -428,7 +471,16 @@ namespace Rml::SolLua { m_children.clear(); // Orphan existing children m_table = newTable; // Update table - bind(false); // Nested rebind + // Update the uservalue to point to the new table + if (m_luaUserdata.valid()) + { + lua_State* L = m_table.lua_state(); + m_luaUserdata.push(L); // [ud] + m_table.push(L); // [ud, tbl] + lua_setuservalue(L, -2); // [ud] + lua_pop(L, 1); // [] + } + bind(false); // Nested rebind } } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index 0071bd0..886e2e6 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -23,13 +23,24 @@ namespace Rml::SolLua DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override; StringList ReflectMemberNames() override; - sol::object get(const sol::object& key) const; - void set(const sol::object& key, sol::object value); + static sol::object luaIndex(SolLuaDataModelProxy& self, sol::stack_object key, sol::this_state ts); + static void luaNewIndex(SolLuaDataModelProxy& self, sol::stack_object key, sol::stack_object value, sol::this_state ts); + + sol::table& table() + { + return m_table; + } + const sol::table& table() const + { + return m_table; + } void bind(bool topLevel); void rebind(const sol::table& newTable); private: + void cacheUserdata(); + SolLuaDataModel* m_datamodel; sol::table m_table; @@ -43,6 +54,9 @@ namespace Rml::SolLua // Not string_view to avoid transient copy since Rml expects String& const std::string* m_topLevelKey = nullptr; + + // Cached Lua userdata for this proxy (used by __index to avoid per-call allocation) + sol::object m_luaUserdata; }; class SolLuaDataModel From d4a419f775e18880370395aed93f62fc715fd341 Mon Sep 17 00:00:00 2001 From: espkk Date: Thu, 26 Mar 2026 04:11:40 +0000 Subject: [PATCH 23/23] Allow datamodel proxy to be used as expression --- src/bind/Context.cpp | 9 +++---- src/bind/bind.cpp | 10 +++++--- src/plugin/SolLuaDataModel.cpp | 45 +++++++++++++++++++--------------- src/plugin/SolLuaDataModel.h | 17 +++++-------- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/bind/Context.cpp b/src/bind/Context.cpp index 027848d..6091261 100644 --- a/src/bind/Context.cpp +++ b/src/bind/Context.cpp @@ -80,14 +80,11 @@ namespace Rml::SolLua } auto dataModel = std::make_shared(model.as(), constructor); + auto& proxy = dataModel->topLevelProxy(); // Alias data model to its top level proxy and push as shared_ptr userdata. - lua_State* L = s; - sol::object obj = sol::make_object(L, std::shared_ptr(dataModel, &dataModel->topLevelProxy())); - obj.push(L); - dataModel->topLevelProxy().table().push(L); - lua_setuservalue(L, -2); - lua_pop(L, 1); + sol::object obj = sol::make_object(s, std::shared_ptr(dataModel, &proxy)); + proxy.attachUservalueTo(obj); return obj; } } // namespace datamodel diff --git a/src/bind/bind.cpp b/src/bind/bind.cpp index 5e20afe..2e4af95 100644 --- a/src/bind/bind.cpp +++ b/src/bind/bind.cpp @@ -5,6 +5,7 @@ #include SOLHPP #include "bind.h" +#include "plugin/SolLuaDataModel.h" namespace Rml::SolLua { @@ -40,9 +41,12 @@ namespace Rml::SolLua case Rml::Variant::VECTOR2: return sol::make_object_userdata(s, variant->Get()); case Rml::Variant::VOIDPTR: - return sol::make_object(s, variant->Get()); - default: - return sol::make_object(s, sol::nil); + { + // The only place where we pass void* to Lua is for the data model proxy. + auto* proxy = static_cast(variant->Get()); + return proxy->luaUserdata(); + } + default:; } return sol::make_object(s, sol::nil); diff --git a/src/plugin/SolLuaDataModel.cpp b/src/plugin/SolLuaDataModel.cpp index 04d5714..32932ae 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -108,9 +108,10 @@ namespace Rml::SolLua { if (ptr == nullptr) { - // TODO: alternatively, the table can be serialized to a string and returned - Rml::Log::Message(Rml::Log::LT_ERROR, "[LUA][ERROR] Trying to access a table as a scalar from VariableDefinition::Get"); - return false; + // Allow RmlUi's expression engine to pass a proxy to an event handler. + // luaUserdata() will be called lazily during variant conversion. + variant = this; + return true; } sol::object obj; @@ -285,18 +286,29 @@ namespace Rml::SolLua return names; } + sol::object& SolLuaDataModelProxy::luaUserdata() + { + if (!m_luaUserdata.valid()) + { + cacheUserdata(); + } + return m_luaUserdata; + } + + void SolLuaDataModelProxy::attachUservalueTo(sol::object& target) const + { + lua_State* L = m_table.lua_state(); + target.push(L); // [ud] + m_table.push(L); // [ud, tbl] + lua_setuservalue(L, -2); // [ud] + lua_pop(L, 1); // [] + } + void SolLuaDataModelProxy::cacheUserdata() { lua_State* L = m_table.lua_state(); m_luaUserdata = sol::make_object_userdata(L, this); - - // Attach the backing Lua table as the userdata's uservalue - { - m_luaUserdata.push(L); // [ud] - m_table.push(L); // [ud, tbl] - lua_setuservalue(L, -2); // [ud] - lua_pop(L, 1); // [] - } + attachUservalueTo(m_luaUserdata); } sol::object SolLuaDataModelProxy::luaIndex(SolLuaDataModelProxy& self, sol::stack_object key, sol::this_state ts) @@ -307,9 +319,7 @@ namespace Rml::SolLua auto it = self.m_children.find(skey); if (it != self.m_children.end()) { - if (!it->second.m_luaUserdata.valid()) - it->second.cacheUserdata(); - return it->second.m_luaUserdata; + return it->second.luaUserdata(); } // Raw lookup in the uservalue table (the proxy's backing Lua table) @@ -471,14 +481,9 @@ namespace Rml::SolLua { m_children.clear(); // Orphan existing children m_table = newTable; // Update table - // Update the uservalue to point to the new table if (m_luaUserdata.valid()) { - lua_State* L = m_table.lua_state(); - m_luaUserdata.push(L); // [ud] - m_table.push(L); // [ud, tbl] - lua_setuservalue(L, -2); // [ud] - lua_pop(L, 1); // [] + attachUservalueTo(m_luaUserdata); } bind(false); // Nested rebind } diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index 886e2e6..055944a 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -16,24 +16,19 @@ namespace Rml::SolLua class SolLuaDataModelProxy final : public Rml::VariableDefinition { public: + static sol::object luaIndex(SolLuaDataModelProxy& self, sol::stack_object key, sol::this_state ts); + static void luaNewIndex(SolLuaDataModelProxy& self, sol::stack_object key, sol::stack_object value, sol::this_state ts); + SolLuaDataModelProxy(SolLuaDataModel* datamodel, sol::table table); + bool Get(void* ptr, Rml::Variant& variant) override; bool Set(void* ptr, const Rml::Variant& variant) override; int Size(void* ptr) override; DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override; StringList ReflectMemberNames() override; - static sol::object luaIndex(SolLuaDataModelProxy& self, sol::stack_object key, sol::this_state ts); - static void luaNewIndex(SolLuaDataModelProxy& self, sol::stack_object key, sol::stack_object value, sol::this_state ts); - - sol::table& table() - { - return m_table; - } - const sol::table& table() const - { - return m_table; - } + void attachUservalueTo(sol::object& target) const; + sol::object& luaUserdata(); void bind(bool topLevel); void rebind(const sol::table& newTable);