1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-06-24 17:11:33 +00:00

Move implementation of UI Content to Lua

This commit is contained in:
uramer 2023-01-29 17:06:09 +01:00
parent 22c62a8c38
commit fc1430af95
7 changed files with 326 additions and 192 deletions

View file

@ -97,46 +97,6 @@ namespace MWLua
sol::table initUserInterfacePackage(const Context& context) sol::table initUserInterfacePackage(const Context& context)
{ {
auto uiContent = context.mLua->sol().new_usertype<LuaUi::Content>("UiContent");
uiContent[sol::meta_function::length] = [](const LuaUi::Content& content) { return content.size(); };
uiContent[sol::meta_function::index]
= sol::overload([](const LuaUi::Content& content, size_t index) { return content.at(fromLuaIndex(index)); },
[](const LuaUi::Content& content, std::string_view name) { return content.at(name); });
uiContent[sol::meta_function::new_index]
= sol::overload([](LuaUi::Content& content, size_t index,
const sol::table& table) { content.assign(fromLuaIndex(index), table); },
[](LuaUi::Content& content, size_t index, sol::nil_t nil) { content.remove(fromLuaIndex(index)); },
[](LuaUi::Content& content, std::string_view name, const sol::table& table) {
content.assign(name, table);
},
[](LuaUi::Content& content, std::string_view name, sol::nil_t nil) { content.remove(name); });
uiContent["insert"] = [](LuaUi::Content& content, size_t index, const sol::table& table) {
content.insert(fromLuaIndex(index), table);
};
uiContent["add"]
= [](LuaUi::Content& content, const sol::table& table) { content.insert(content.size(), table); };
uiContent["indexOf"] = [](const LuaUi::Content& content, const sol::table& table) -> sol::optional<size_t> {
size_t index = content.indexOf(table);
if (index < content.size())
return toLuaIndex(index);
else
return sol::nullopt;
};
{
auto pairs = [](const LuaUi::Content& content) {
auto next
= [](const LuaUi::Content& content, size_t i) -> sol::optional<std::tuple<size_t, sol::table>> {
if (i < content.size())
return std::make_tuple(i + 1, content.at(i));
else
return sol::nullopt;
};
return std::make_tuple(next, content, 0);
};
uiContent[sol::meta_function::ipairs] = pairs;
uiContent[sol::meta_function::pairs] = pairs;
}
auto element = context.mLua->sol().new_usertype<LuaUi::Element>("Element"); auto element = context.mLua->sol().new_usertype<LuaUi::Element>("Element");
element["layout"] = sol::property([](LuaUi::Element& element) { return element.mLayout; }, element["layout"] = sol::property([](LuaUi::Element& element) { return element.mLayout; },
[](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; });
@ -181,7 +141,7 @@ namespace MWLua
luaManager->addAction([wm, obj = obj.as<LObject>()] { wm->setConsoleSelectedObject(obj.ptr()); }); luaManager->addAction([wm, obj = obj.as<LObject>()] { wm->setConsoleSelectedObject(obj.ptr()); });
} }
}; };
api["content"] = [](const sol::table& table) { return LuaUi::Content(table); }; api["content"] = LuaUi::Content::makeFactory(context.mLua->sol());
api["create"] = [context](const sol::table& layout) { api["create"] = [context](const sol::table& layout) {
auto element = LuaUi::Element::make(layout); auto element = LuaUi::Element::make(layout);
context.mLuaManager->addAction(std::make_unique<UiAction>(UiAction::CREATE, element, context.mLua)); context.mLuaManager->addAction(std::make_unique<UiAction>(UiAction::CREATE, element, context.mLua));

View file

@ -1,62 +1,96 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <sol/sol.hpp> #include <sol/sol.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua_ui/content.hpp> #include <components/lua_ui/content.hpp>
namespace namespace
{ {
using namespace testing; using namespace testing;
sol::state state; struct LuaUiContentTest : Test
sol::table makeTable()
{ {
return sol::table(state, sol::create); LuaUtil::LuaState mLuaState{ nullptr, nullptr };
} sol::state_view mSol;
sol::protected_function mNew;
LuaUiContentTest()
: mSol(mLuaState.sol())
, mNew(LuaUi::Content::makeFactory(mSol))
{
mSol.open_libraries(sol::lib::base, sol::lib::table);
}
sol::table makeTable(std::string name) LuaUi::Content::View makeContent(sol::table source)
{ {
auto result = makeTable(); auto result = mNew.call(source);
result["name"] = name; if (result.get_type() != sol::type::table)
return result; throw std::logic_error("Expected table");
} return LuaUi::Content::View(result.get<sol::table>());
}
TEST(LuaUiContentTest, Create) sol::table makeTable() { return sol::table(mSol, sol::create); }
sol::table makeTable(std::string name)
{
auto result = makeTable();
result["name"] = name;
return result;
}
};
TEST_F(LuaUiContentTest, Create)
{ {
auto table = makeTable(); auto table = makeTable();
table.add(makeTable()); table.add(makeTable());
table.add(makeTable()); table.add(makeTable());
table.add(makeTable()); table.add(makeTable());
LuaUi::Content content(table); LuaUi::Content::View content = makeContent(table);
EXPECT_EQ(content.size(), 3); EXPECT_EQ(content.size(), 3);
} }
TEST(LuaUiContentTest, CreateWithHole) TEST_F(LuaUiContentTest, Insert)
{ {
auto table = makeTable(); auto table = makeTable();
table.add(makeTable()); table.add(makeTable());
table.add(makeTable()); table.add(makeTable());
table[4] = makeTable(); table.add(makeTable());
EXPECT_ANY_THROW(LuaUi::Content content(table)); LuaUi::Content::View content = makeContent(table);
content.insert(2, makeTable("inserted"));
EXPECT_EQ(content.size(), 4);
auto inserted = content.at("inserted");
auto index = content.indexOf(inserted);
EXPECT_TRUE(index.has_value());
EXPECT_EQ(index.value(), 2);
} }
TEST(LuaUiContentTest, WrongType) TEST_F(LuaUiContentTest, MakeHole)
{
auto table = makeTable();
table.add(makeTable());
table.add(makeTable());
LuaUi::Content::View content = makeContent(table);
sol::table t = makeTable();
EXPECT_ANY_THROW(content.assign(3, t));
}
TEST_F(LuaUiContentTest, WrongType)
{ {
auto table = makeTable(); auto table = makeTable();
table.add(makeTable()); table.add(makeTable());
table.add("a"); table.add("a");
table.add(makeTable()); table.add(makeTable());
EXPECT_ANY_THROW(LuaUi::Content content(table)); EXPECT_ANY_THROW(makeContent(table));
} }
TEST(LuaUiContentTest, NameAccess) TEST_F(LuaUiContentTest, NameAccess)
{ {
auto table = makeTable(); auto table = makeTable();
table.add(makeTable()); table.add(makeTable());
table.add(makeTable("a")); table.add(makeTable("a"));
LuaUi::Content content(table); LuaUi::Content::View content = makeContent(table);
EXPECT_NO_THROW(content.at("a")); EXPECT_NO_THROW(content.at("a"));
content.remove("a"); content.remove("a");
EXPECT_EQ(content.size(), 1);
content.assign(content.size(), makeTable("b")); content.assign(content.size(), makeTable("b"));
content.assign("b", makeTable()); content.assign("b", makeTable());
EXPECT_ANY_THROW(content.at("b")); EXPECT_ANY_THROW(content.at("b"));
@ -67,31 +101,33 @@ namespace
EXPECT_ANY_THROW(content.at("c")); EXPECT_ANY_THROW(content.at("c"));
} }
TEST(LuaUiContentTest, IndexOf) TEST_F(LuaUiContentTest, IndexOf)
{ {
auto table = makeTable(); auto table = makeTable();
table.add(makeTable()); table.add(makeTable());
table.add(makeTable()); table.add(makeTable());
table.add(makeTable()); table.add(makeTable());
LuaUi::Content content(table); LuaUi::Content::View content = makeContent(table);
auto child = makeTable(); auto child = makeTable();
content.assign(2, child); content.assign(2, child);
EXPECT_EQ(content.indexOf(child), 2); EXPECT_EQ(content.indexOf(child).value(), 2);
EXPECT_EQ(content.indexOf(makeTable()), content.size()); EXPECT_TRUE(!content.indexOf(makeTable()).has_value());
} }
TEST(LuaUiContentTest, BoundsChecks) TEST_F(LuaUiContentTest, BoundsChecks)
{ {
auto table = makeTable(); auto table = makeTable();
LuaUi::Content content(table); LuaUi::Content::View content = makeContent(table);
EXPECT_ANY_THROW(content.at(0)); EXPECT_ANY_THROW(content.at(0));
content.assign(content.size(), makeTable()); content.assign(content.size(), makeTable());
content.assign(content.size(), makeTable()); content.assign(content.size(), makeTable());
content.assign(content.size(), makeTable()); content.assign(content.size(), makeTable());
EXPECT_EQ(content.size(), 3);
EXPECT_ANY_THROW(content.at(3)); EXPECT_ANY_THROW(content.at(3));
EXPECT_ANY_THROW(content.remove(3)); EXPECT_ANY_THROW(content.remove(3));
EXPECT_NO_THROW(content.remove(1)); EXPECT_NO_THROW(content.remove(1)); // TODO: something cursed happens here, even __newindex is not called!
EXPECT_NO_THROW(content.at(1)); EXPECT_EQ(content.size(), 2);
EXPECT_NO_THROW(content.at(2));
EXPECT_EQ(content.size(), 2); EXPECT_EQ(content.size(), 2);
} }
} }

View file

@ -272,6 +272,7 @@ add_component_dir (lua_ui
properties widget element util layers content alignment resources properties widget element util layers content alignment resources
adapter text textedit window image container flex adapter text textedit window image container flex
) )
list (APPEND OPENMW_FILES "lua_ui/content.lua")
if(WIN32) if(WIN32)

View file

@ -1,108 +1,33 @@
#include "content.hpp" #include "content.hpp"
namespace LuaUi namespace LuaUi::Content
{ {
int64_t Content::sInstanceCount = 0; namespace
Content::Content(const sol::table& table)
{ {
sInstanceCount++; sol::table loadMetatable(sol::state_view sol)
size_t size = table.size();
for (size_t index = 0; index < size; ++index)
{ {
sol::object value = table.get<sol::object>(index + 1); std::string scriptBody =
if (value.is<sol::table>()) #include "content.lua"
assign(index, value.as<sol::table>()); ;
else auto result = sol.safe_script(scriptBody);
throw std::logic_error("UI Content children must all be tables."); if (result.get_type() != sol::type::table)
throw std::logic_error("Expected a meta table");
return result.get<sol::table>();
} }
} }
void Content::assign(size_t index, const sol::table& table) sol::protected_function makeFactory(sol::state_view sol)
{ {
if (mOrdered.size() < index) sol::table metatable = loadMetatable(sol);
throw std::logic_error("Can't have gaps in UI Content."); if (metatable["new"].get_type() != sol::type::function)
if (index == mOrdered.size()) throw std::logic_error("Expected function");
mOrdered.push_back(table); return metatable["new"].get<sol::protected_function>();
else
{
sol::optional<std::string> oldName = mOrdered[index]["name"];
if (oldName.has_value())
mNamed.erase(oldName.value());
mOrdered[index] = table;
}
sol::optional<std::string> name = table["name"];
if (name.has_value())
mNamed[name.value()] = index;
} }
void Content::assign(std::string_view name, const sol::table& table) int64_t View::sInstanceCount = 0;
{
auto it = mNamed.find(name);
if (it != mNamed.end())
assign(it->second, table);
else
throw std::logic_error(std::string("Can't find a UI Content child with name ") += name);
}
void Content::insert(size_t index, const sol::table& table) int64_t getInstanceCount()
{ {
if (mOrdered.size() < index) return View::sInstanceCount;
throw std::logic_error("Can't have gaps in UI Content.");
mOrdered.insert(mOrdered.begin() + index, table);
for (size_t i = index; i < mOrdered.size(); ++i)
{
sol::optional<std::string> name = mOrdered[i]["name"];
if (name.has_value())
mNamed[name.value()] = index;
}
}
sol::table Content::at(size_t index) const
{
if (index > size())
throw std::logic_error("Invalid UI Content index.");
return mOrdered.at(index);
}
sol::table Content::at(std::string_view name) const
{
auto it = mNamed.find(name);
if (it == mNamed.end())
throw std::logic_error("Invalid UI Content name.");
return mOrdered.at(it->second);
}
size_t Content::remove(size_t index)
{
sol::table table = at(index);
sol::optional<std::string> name = table["name"];
if (name.has_value())
{
auto it = mNamed.find(name.value());
if (it != mNamed.end())
mNamed.erase(it);
}
mOrdered.erase(mOrdered.begin() + index);
return index;
}
size_t Content::remove(std::string_view name)
{
auto it = mNamed.find(name);
if (it == mNamed.end())
throw std::logic_error("Invalid UI Content name.");
size_t index = it->second;
remove(index);
return index;
}
size_t Content::indexOf(const sol::table& table) const
{
auto it = std::find(mOrdered.begin(), mOrdered.end(), table);
if (it == mOrdered.end())
return size();
else
return it - mOrdered.begin();
} }
} }

View file

@ -6,52 +6,120 @@
#include <sol/sol.hpp> #include <sol/sol.hpp>
namespace LuaUi namespace LuaUi::Content
{ {
class Content sol::protected_function makeFactory(sol::state_view);
class View
{ {
public: public:
using iterator = std::vector<sol::table>::iterator; static int64_t sInstanceCount; // debug information, shown in Lua profiler
Content() { sInstanceCount++; } // accepts only Lua tables returned by ui.content
~Content() { sInstanceCount--; } explicit View(sol::table table)
Content(const Content& c) : mTable(std::move(table))
{ {
this->mNamed = c.mNamed; if (!isValid(mTable))
this->mOrdered = c.mOrdered; throw std::domain_error("Expected a Content table");
sInstanceCount++; sInstanceCount++;
} }
Content(Content&& c) View(const View& c)
{ {
this->mNamed = std::move(c.mNamed); this->mTable = c.mTable;
this->mOrdered = std::move(c.mOrdered);
sInstanceCount++; sInstanceCount++;
} }
View(View&& c)
{
this->mTable = std::move(c.mTable);
sInstanceCount++;
}
~View() { sInstanceCount--; }
// expects a Lua array - a table with keys from 1 to n without any nil values in between static bool isValid(const sol::object& object)
// any other keys are ignored {
explicit Content(const sol::table&); if (object.get_type() != sol::type::table)
return false;
sol::table table = object;
return table.traverse_get<sol::optional<bool>>(sol::metatable_key, "__Content").value_or(false);
}
size_t size() const { return mOrdered.size(); } size_t size() const { return mTable.size(); }
void assign(std::string_view name, const sol::table& table); void assign(std::string_view name, const sol::table& table)
void assign(size_t index, const sol::table& table); {
void insert(size_t index, const sol::table& table); if (indexOf(name).has_value())
mTable[name] = table;
else
throw std::domain_error("Invalid Content key");
}
void assign(size_t index, const sol::table& table)
{
if (index <= size())
mTable[toLua(index)] = table;
else
throw std::domain_error("Invalid Content index");
}
void insert(size_t index, const sol::table& table) { callMethod("insert", toLua(index), table); }
sol::table at(size_t index) const; sol::table at(size_t index) const
sol::table at(std::string_view name) const; {
size_t remove(size_t index); if (index < size())
size_t remove(std::string_view name); return mTable.get<sol::table>(toLua(index));
size_t indexOf(const sol::table& table) const; else
throw std::domain_error("Invalid Content index");
static int64_t getInstanceCount() { return sInstanceCount; } }
sol::table at(std::string_view name) const
{
if (indexOf(name).has_value())
return mTable.get<sol::table>(name);
else
throw std::domain_error("Invalid Content key");
}
void remove(size_t index)
{
if (index < size())
mTable[toLua(index)] = sol::nil;
else
throw std::domain_error("Invalid Content index");
}
void remove(std::string_view name)
{
if (indexOf(name).has_value())
mTable[name] = sol::nil;
else
throw std::domain_error("Invalid Content index");
}
std::optional<size_t> indexOf(std::string_view name) const
{
sol::object result = callMethod("indexOf", name);
if (result.is<size_t>())
return fromLua(result.as<size_t>());
else
return std::nullopt;
}
std::optional<size_t> indexOf(const sol::table& table) const
{
sol::object result = callMethod("indexOf", table);
if (result.is<size_t>())
return fromLua(result.as<size_t>());
else
return std::nullopt;
}
private: private:
std::map<std::string, size_t, std::less<>> mNamed; sol::table mTable;
std::vector<sol::table> mOrdered;
static int64_t sInstanceCount; // debug information, shown in Lua profiler template <typename... Arg>
sol::object callMethod(std::string_view name, Arg&&... arg) const
{
return mTable.get<sol::protected_function>(name)(mTable, arg...);
}
static inline size_t toLua(size_t index) { return index + 1; }
static inline size_t fromLua(size_t index) { return index - 1; }
}; };
int64_t getInstanceCount();
} }
#endif // COMPONENTS_LUAUI_CONTENT #endif // COMPONENTS_LUAUI_CONTENT

View file

@ -0,0 +1,144 @@
R"(
local M = {}
M.__Content = true
M.new = function(source)
local result = {}
result.__nameIndex = {}
for i, v in ipairs(source) do
if type(v) ~= 'table' then
error("Content can only contain tables")
end
result[i] = v
if type(v.name) == 'string' then
result.__nameIndex[v.name] = i
end
end
return setmetatable(result, M)
end
local function validateIndex(self, index)
if type(index) ~= 'number' then
error('Unexpected Content key: ' .. tostring(index))
end
if index < 1 or (#self + 1) < index then
error('Invalid Content index: ' .. tostring(index))
end
end
local function getIndexFromKey(self, key)
local index = key
if type(key) == 'string' then
index = self.__nameIndex[key]
if not index then
error("Unexpected content key:" .. key)
end
end
validateIndex(self, index)
return index
end
local function nameAt(self, index)
local v = rawget(self, index)
return v and type(v.name) == 'string' and v.name
end
local methods = {
insert = function(self, index, value)
validateIndex(self, index)
if type(value) ~= 'table' then
error('Content can only contain tables')
end
for i = #self, index, -1 do
rawset(self, i + 1, rawget(self, i))
local name = rawget(self, i + 1)
if name then
self.__nameIndex[name] = i + 1
end
end
rawset(self, index, value)
if value.name then
self.__nameIndex[value.name] = index
end
end,
indexOf = function(self, value)
if type(value) == 'string' then
return self.__nameIndex[value]
elseif type(value) == 'table' then
for i = 1, #self do
if rawget(self, i) == value then
return i
end
end
end
return nil
end,
add = function(self, value)
self:insert(#self + 1, value)
return #self
end,
}
M.__index = function(self, key)
if methods[key] then return methods[key] end
local index = getIndexFromKey(self, key)
return rawget(self, index)
end
local function remove(self, index)
print('remove', #self, index)
local oldName = nameAt(self, index)
if oldName then
self.__nameIndex[oldName] = nil
end
if index > #self then
error('Invalid Content index:' .. tostring(index))
end
for i = index, #self - 1 do
local v = rawget(self, i + 1)
rawset(self, i, v)
if type(v.name) == 'string' then
self.__nameIndex[v.name] = i
end
end
rawset(self, #self, nil)
print('removed', #self)
end
local function assign(self, index, value)
local oldName = nameAt(self, index)
if oldName then
self.__nameIndex[oldName] = nil
end
rawset(self, index, value)
if value.name then
self.__nameIndex[value.name] = index
end
end
M.__newindex = function(self, key, value)
print('__newindex ', key, value)
local index = getIndexFromKey(self, key)
if value == nil then
remove(self, index)
elseif type(value) == 'table' then
assign(self, index, value)
else
error('Content can only contain tables')
end
end
local function next(self, index)
local v = rawget(self, index)
if v then
return index + 1, v
else
return nil, nil
end
end
M.__pairs = function(self)
return next, self, 1
end
M.__ipairs = M.__pairs
local test = M.new({})
test:insert(1, {})
test[2] = {}
assert(#test == 2, "Wrong size")
test:add({ name = 'a' })
assert(getIndexFromKey(test, 'a') == 3, getIndexFromKey(test, 'a'))
assert(type(test.a) == 'table', type(test.a))
assert(test.a.name == 'a', 'wrong table')
return M
)"

View file

@ -63,9 +63,9 @@ namespace LuaUi
destroyWidget(w); destroyWidget(w);
return result; return result;
} }
if (!contentObj.is<Content>()) if (!Content::View::isValid(contentObj))
throw std::logic_error("Layout content field must be a openmw.ui.content"); throw std::logic_error("Layout content field must be a openmw.ui.content");
const Content& content = contentObj.as<Content>(); Content::View content(contentObj.as<sol::table>());
result.resize(content.size()); result.resize(content.size());
size_t minSize = std::min(children.size(), content.size()); size_t minSize = std::min(children.size(), content.size());
for (size_t i = 0; i < minSize; i++) for (size_t i = 0; i < minSize; i++)