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. diff --git a/src/bind/Context.cpp b/src/bind/Context.cpp index b726501..6091261 100644 --- a/src/bind/Context.cpp +++ b/src/bind/Context.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include @@ -49,44 +48,6 @@ namespace Rml::SolLua 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. @@ -96,34 +57,35 @@ namespace Rml::SolLua /// 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) + static sol::object + openDataModel(Rml::Context& self, const Rml::String& name, sol::object model, sol::this_state s) { - sol::state_view lua{s}; + if (model.get_type() != sol::type::table) + { + Rml::Log::Message(Log::LT_ERROR, "Data model must be a table."); + return sol::make_object(s, sol::lua_nil); + } // 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; + { + return sol::make_object(s, sol::lua_nil); + } } - 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); - } + auto dataModel = std::make_shared(model.as(), constructor); + auto& proxy = dataModel->topLevelProxy(); - return data; + // Alias data model to its top level proxy and push as shared_ptr userdata. + sol::object obj = sol::make_object(s, std::shared_ptr(dataModel, &proxy)); + proxy.attachUservalueTo(obj); + return obj; } } // namespace datamodel @@ -179,18 +141,19 @@ namespace Rml::SolLua "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+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) + // 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) ); // clang-format on } diff --git a/src/bind/DataModel.cpp b/src/bind/DataModel.cpp index 16d9702..e294da2 100644 --- a/src/bind/DataModel.cpp +++ b/src/bind/DataModel.cpp @@ -1,5 +1,3 @@ -#include - #include #include SOLHPP @@ -7,28 +5,46 @@ 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(SolLuaDataModel& self, const std::string& name, sol::this_state s) + static int dataModelPairs(lua_State* L) // [-0, +3] { - return self.Table.get(name); + 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(SolLuaDataModel& self, const std::string& name, sol::object value, sol::this_state s) + static int dataModelIpairs(lua_State* L) // [-0, +3] { - self.Handle.DirtyVariable(name); - self.Table.set(name, 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 int dataModelLen(lua_State* L) // [-0, +1] + { + lua_getuservalue(L, 1); // [tbl] + lua_pushinteger(L, static_cast(lua_rawlen(L, -1))); // [tbl, len] + return 1; } } // namespace functions void bind_datamodel(sol::state_view& lua) { // clang-format off - lua.new_usertype("SolLuaDataModel", sol::no_constructor, - sol::meta_function::index, &functions::dataModelGet, - sol::meta_function::new_index, &functions::dataModelSet + lua.new_usertype("SolLuaDataModelProxy", sol::no_constructor, + 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 } - } // end namespace Rml::SolLua 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 30352c7..32932ae 100644 --- a/src/plugin/SolLuaDataModel.cpp +++ b/src/plugin/SolLuaDataModel.cpp @@ -1,133 +1,491 @@ +#include +#include +#include +#include +#include #include +#include +#include +#include +#include #include #include +#include #include SOLHPP +#include #include "SolLuaDataModel.h" +#include "SolLuaDocument.h" +#include "bind/bind.h" + namespace Rml::SolLua { - SolLuaDataModel::SolLuaDataModel(sol::state_view s) : Lua{s} - {} + 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); + } + + bool Set(void* ptr, const Variant& variant) override + { + return m_self->Set(ptr, variant); + } + + SolLuaDataModelProxy* m_self; + }; + + 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 def; + 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) + : m_constructor(constructor), + m_topLevelProxy(this, model) + { + m_topLevelProxy.bind(true); + } + + Rml::DataModelConstructor SolLuaDataModel::constructor() const + { + return m_constructor; + } - SolLuaObjectDef::SolLuaObjectDef(SolLuaDataModel* model) - : VariableDefinition(DataVariableType::Scalar), m_model(model) + SolLuaDataModelProxy& SolLuaDataModel::topLevelProxy() { + return m_topLevelProxy; } - bool SolLuaObjectDef::Get(void* ptr, Rml::Variant& variant) + /// SolLuaDatamodelProxy + SolLuaDataModelProxy::SolLuaDataModelProxy(SolLuaDataModel* datamodel, sol::table table) + : VariableDefinition(DataVariableType::Struct), + m_datamodel(datamodel), m_table(std::move(table)), m_selfAsScalar(std::make_unique(this)) { - auto obj = static_cast(ptr); + } + + bool SolLuaDataModelProxy::Get(void* ptr, Rml::Variant& variant) + { + if (ptr == nullptr) + { + // 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; + } - 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) + sol::object obj; + auto* key = const_cast(static_cast(ptr)); + if (IsArrayIndex(key)) + { + // Pseudo-key: access by index + int 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()) + { + 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 + } + 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) + bool SolLuaDataModelProxy::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); + 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 (IsArrayIndex(key)) + { + // Pseudo-key: access by index + int 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()) + { + 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 + 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) + int SolLuaDataModelProxy::Size(void* ptr) { - // Non-table types are 1 entry long. - auto object = static_cast(ptr); - if (object->get_type() != sol::type::table) - return 1; + 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()); + } - auto t = object->as(); - return static_cast(t.size()); + DataVariable SolLuaDataModelProxy::Child(void* ptr, const Rml::DataAddressEntry& address) + { + 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) + { + // 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"); + return {}; + } + // Access by index + skey = std::format("[{}]", address.index); + obj = m_table[address.index + 1]; // Lua is 1-based + } + else if (IsArrayIndex(address.name)) + { + // 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") + { + return MakeLiteralIntVariable(m_table.size()); + } + + skey = address.name; + obj = m_table[address.name]; + } + + 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); + if (it == m_keys.end()) + { + // Key is not in the proxy + return {}; + } + 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::luaUserdata() + { + if (!m_luaUserdata.valid()) + { + cacheUserdata(); + } + return m_luaUserdata; } - DataVariable SolLuaObjectDef::Child(void* ptr, const Rml::DataAddressEntry& address) + void SolLuaDataModelProxy::attachUservalueTo(sol::object& target) const { - // Child should be called on a table. - auto object = static_cast(ptr); - if (object->get_type() != sol::type::table) - return DataVariable{}; + 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); // [] + } - // 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())); + void SolLuaDataModelProxy::cacheUserdata() + { + lua_State* L = m_table.lua_state(); + m_luaUserdata = sol::make_object_userdata(L, this); + attachUservalueTo(m_luaUserdata); + } + + 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); - // Accessing by name. - if (address.index == -1) + auto it = self.m_children.find(skey); + if (it != self.m_children.end()) { - // Try to get the object. - auto e = table.get(address.name); - if (e.get_type() == sol::type::lua_nil) - return DataVariable{}; + return it->second.luaUserdata(); + } - // 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)}; + // Raw lookup in the uservalue table (the proxy's backing Lua table) + { + 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; + } + } + + 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); + + // 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 = self.m_children.find(skey); + if (proxyTableIt == self.m_children.end() && IsArrayIndex(skey) && self.m_topLevelKey != nullptr) + { + // New array element + auto childProxyIt = self.m_children.try_emplace(skey, self.m_datamodel, value.as()); + RMLUI_ASSERT(childProxyIt.second); + 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 + proxyTableIt->second.rebind(value.as()); + } } - // 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) + if (!self.m_keys.contains(skey)) { - 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)}; + // 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 (self.m_topLevelKey != nullptr) + { + self.m_keys.emplace(skey); + } } + } + + self.m_datamodel->constructor().GetModelHandle().DirtyVariable(self.m_topLevelKey ? *self.m_topLevelKey : skey); + } - // Iterate through the entries and grab the nth entry. - int idx = 0; - for (auto& [k, v] : table.pairs()) + void SolLuaDataModelProxy::bind(bool topLevel) + { + for (auto& [key, value] : m_table) + { + std::string skey; + if (key.get_type() == sol::type::string) { - if (idx == address.index) + skey = key.as(); + } + else if (key.get_type() == sol::type::number) + { + const double number = key.as() - 1; + const bool isInteger = std::isfinite(number) && std::floor(number) == number; + if (isInteger && number >= 0) { - auto it = m_model->ObjectList.insert_or_assign(tablestr + "_" + std::to_string(idx), v); - return DataVariable{m_model->ObjectDef.get(), &(it.first->second)}; + // 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 } - ++idx; + } + if (skey.empty()) + { + Rml::Log::Message(Log::LT_ERROR, "[LUA][ERROR] Data model key other than non-empty string or non-negative integer is unsupported"); + return; } - // Index out of range. - return DataVariable{}; + 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 && !IsArrayIndex(skey)) + { + // Only bind top-level non-integer keys + m_datamodel->constructor().BindCustomDataVariable( + skey, Rml::DataVariable(&childProxyIt.first->second, nullptr) + ); + } + } + else + { + if (value.get_type() == sol::type::function) + { + if (!topLevel) + { + Rml::Log::Message( + Log::LT_WARNING, "[LUA][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 && !IsArrayIndex(skey)) + { + // Only bind top-level non-integer keys + m_datamodel->constructor().BindCustomDataVariable( + skey, Rml::DataVariable(m_selfAsScalar.get(), const_cast(it.first->data())) + ); + } + } + } } + } - // Failure. - return DataVariable{}; + void SolLuaDataModelProxy::rebind(const sol::table& newTable) + { + m_children.clear(); // Orphan existing children + m_table = newTable; // Update table + if (m_luaUserdata.valid()) + { + attachUservalueTo(m_luaUserdata); + } + bind(false); // Nested rebind } } // end namespace Rml::SolLua diff --git a/src/plugin/SolLuaDataModel.h b/src/plugin/SolLuaDataModel.h index 8043250..055944a 100644 --- a/src/plugin/SolLuaDataModel.h +++ b/src/plugin/SolLuaDataModel.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -10,34 +11,60 @@ namespace Rml::SolLua { - class SolLuaObjectDef; + class SolLuaDataModel; - struct SolLuaDataModel + class SolLuaDataModelProxy final : public Rml::VariableDefinition { - SolLuaDataModel(sol::state_view s); - - Rml::DataModelConstructor Constructor; - Rml::DataModelHandle Handle; - sol::state_view Lua; - std::unique_ptr ObjectDef; + 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); - // sol data types are reference counted. Hold onto them as we use them. - sol::table Table; - std::unordered_map ObjectList; - }; + SolLuaDataModelProxy(SolLuaDataModel* datamodel, sol::table table); - 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; + StringList ReflectMemberNames() override; + + void attachUservalueTo(sol::object& target) const; + sol::object& luaUserdata(); + + void bind(bool topLevel); + void rebind(const sol::table& newTable); + + private: + void cacheUserdata(); + + SolLuaDataModel* m_datamodel; + sol::table m_table; + + std::unique_ptr m_selfAsScalar; + + // Children proxies for nested tables + std::unordered_map m_children; + + // Store keys of non-table values in a set just to keep alive the strings + std::unordered_set m_keys; + + // 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 + { + public: + SolLuaDataModel(const sol::table& model, const Rml::DataModelConstructor& constructor); + + Rml::DataModelConstructor constructor() const; + SolLuaDataModelProxy& topLevelProxy(); - protected: - SolLuaDataModel* m_model; - sol::object m_object; + private: + Rml::DataModelConstructor m_constructor; + SolLuaDataModelProxy m_topLevelProxy; }; } // end namespace Rml::SolLua