1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-06-10 13:41:32 +00:00

Move implementation of UI Content to Lua (!2661 for 0.48)

This commit is contained in:
uramer 2023-01-29 17:06:09 +01:00 committed by Petr Mikheev
parent 15236faf03
commit 4b2ef32b86
8 changed files with 326 additions and 215 deletions

View file

@ -89,69 +89,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"] = [](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 = [](LuaUi::Content& content)
{
auto next = [](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( element["layout"] = sol::property(
[](LuaUi::Element& element) [](LuaUi::Element& element)
@ -210,12 +147,8 @@ 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) api["content"] = LuaUi::loadContentConstructor(context.mLua);
{ api["create"] = [context](const sol::table& layout) {
return LuaUi::Content(table);
};
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));
return element; return element;

View file

@ -1,62 +1,105 @@
#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::protected_function mNew;
LuaUiContentTest()
{
mLuaState.addInternalLibSearchPath("resources/lua_libs");
mNew = LuaUi::loadContentConstructor(&mLuaState);
} }
LuaUi::ContentView makeContent(sol::table source)
{
auto result = mNew.call(source);
if (result.get_type() != sol::type::table)
throw std::logic_error("Expected table");
return LuaUi::ContentView(result.get<sol::table>());
}
sol::table makeTable() { return sol::table(mLuaState.sol(), sol::create); }
sol::table makeTable(std::string name) sol::table makeTable(std::string name)
{ {
auto result = makeTable(); auto result = makeTable();
result["name"] = name; result["name"] = name;
return result; return result;
} }
};
TEST(LuaUiContentTest, Create) TEST_F(LuaUiContentTest, ProtectedMetatable)
{
mLuaState.sol()["makeContent"] = mNew;
mLuaState.sol()["M"] = makeContent(makeTable()).getMetatable();
std::string testScript = R"(
assert(not pcall(function() setmetatable(makeContent{}, {}) end), 'Metatable is not protected')
assert(getmetatable(makeContent{}) ~= M, 'Metatable is not protected')
)";
EXPECT_NO_THROW(mLuaState.sol().safe_script(testScript));
}
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::ContentView 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::ContentView 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::ContentView 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::ContentView 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 +110,35 @@ 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::ContentView 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::ContentView content = makeContent(table);
EXPECT_ANY_THROW(content.at(0)); EXPECT_ANY_THROW(content.at(0));
EXPECT_EQ(content.size(), 0);
content.assign(content.size(), makeTable()); content.assign(content.size(), makeTable());
EXPECT_EQ(content.size(), 1);
content.assign(content.size(), makeTable()); content.assign(content.size(), makeTable());
EXPECT_EQ(content.size(), 2);
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)); content.remove(2);
EXPECT_NO_THROW(content.at(1));
EXPECT_EQ(content.size(), 2); EXPECT_EQ(content.size(), 2);
EXPECT_ANY_THROW(content.at(2));
} }
} }

View file

@ -271,6 +271,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
) )
copy_resource_file("lua_ui/content.lua" "${OPENMW_RESOURCES_ROOT}" "resources/lua_libs/content.lua")
if(WIN32) if(WIN32)

View file

@ -2,104 +2,21 @@
namespace LuaUi namespace LuaUi
{ {
Content::Content(const sol::table& table) sol::protected_function loadContentConstructor(LuaUtil::LuaState* state)
{ {
size_t size = table.size(); sol::function loader = state->loadInternalLib("content");
for (size_t index = 0; index < size; ++index) sol::set_environment(state->newInternalLibEnvironment(), loader);
{ sol::table metatable = loader().get<sol::table>();
sol::object value = table.get<sol::object>(index + 1); if (metatable["new"].get_type() != sol::type::function)
if (value.is<sol::table>()) throw std::logic_error("Expected function");
assign(index, value.as<sol::table>()); return metatable["new"].get<sol::protected_function>();
else
throw std::logic_error("UI Content children must all be tables.");
}
} }
void Content::assign(size_t index, const sol::table& table) bool isValidContent(const sol::object& object)
{ {
if (mOrdered.size() < index) if (object.get_type() != sol::type::table)
throw std::logic_error("Can't have gaps in UI Content."); return false;
if (index == mOrdered.size()) sol::table table = object;
mOrdered.push_back(table); return table.traverse_get<sol::optional<bool>>(sol::metatable_key, "__Content").value_or(false);
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)
{
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)
{
if (mOrdered.size() < index)
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)
{
auto it = std::find(mOrdered.begin(), mOrdered.end(), table);
if (it == mOrdered.end())
return size();
else
return it - mOrdered.begin();
} }
} }

View file

@ -6,36 +6,105 @@
#include <sol/sol.hpp> #include <sol/sol.hpp>
#include <components/lua/luastate.hpp>
namespace LuaUi namespace LuaUi
{ {
class Content sol::protected_function loadContentConstructor(LuaUtil::LuaState* state);
bool isValidContent(const sol::object& object);
class ContentView
{ {
public: public:
using iterator = std::vector<sol::table>::iterator; // accepts only Lua tables returned by ui.content
explicit ContentView(sol::table table)
: mTable(std::move(table))
{
if (!isValidContent(mTable))
throw std::domain_error("Expected a Content table");
}
Content() {} size_t size() const { return mTable.size(); }
// expects a Lua array - a table with keys from 1 to n without any nil values in between void assign(std::string_view name, const sol::table& table)
// any other keys are ignored {
explicit Content(const sol::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::range_error("Invalid Content index");
}
void insert(size_t index, const sol::table& table) { callMethod("insert", toLua(index), table); }
size_t size() const { return mOrdered.size(); } sol::table at(size_t index) const
{
if (index < size())
return mTable.get<sol::table>(toLua(index));
else
throw std::range_error("Invalid Content index");
}
sol::table at(std::string_view name) const
{
if (indexOf(name).has_value())
return mTable.get<sol::table>(name);
else
throw std::range_error("Invalid Content key");
}
void remove(size_t index)
{
if (index < size())
// for some reason mTable[key] = value doesn't call __newindex
getMetatable()[sol::meta_function::new_index].get<sol::protected_function>()(
mTable, toLua(index), sol::nil);
else
throw std::range_error("Invalid Content index");
}
void remove(std::string_view name)
{
auto index = indexOf(name);
if (index.has_value())
remove(index.value());
else
throw std::domain_error("Invalid Content key");
}
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;
}
void assign(std::string_view name, const sol::table& table); sol::table getMetatable() const { return mTable[sol::metatable_key].get<sol::table>(); }
void assign(size_t index, const sol::table& table);
void insert(size_t index, const sol::table& table);
sol::table at(size_t index) const;
sol::table at(std::string_view name) const;
size_t remove(size_t index);
size_t remove(std::string_view name);
size_t indexOf(const sol::table& table);
private: private:
std::map<std::string, size_t, std::less<>> mNamed; sol::table mTable;
std::vector<sol::table> mOrdered;
};
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; }
};
} }
#endif // COMPONENTS_LUAUI_CONTENT #endif // COMPONENTS_LUAUI_CONTENT

View file

@ -0,0 +1,140 @@
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 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 nameAt(self, index)
local v = rawget(self, index)
return v and type(v.name) == 'string' and v.name
end
local function 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)
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)
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
M.__tostring = function(self)
return ('UiContent{%d layouts}'):format(#self)
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
M.__metatable = {}
return M

View file

@ -62,9 +62,7 @@ namespace LuaUi
destroyWidget(w); destroyWidget(w);
return result; return result;
} }
if (!contentObj.is<Content>()) ContentView content(contentObj.as<sol::table>());
throw std::logic_error("Layout content field must be a openmw.ui.content");
Content content = contentObj.as<Content>();
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++)

View file

@ -172,6 +172,12 @@
-- for i = 1, #content do -- for i = 1, #content do
-- print('widget',content[i].name,'at',i) -- print('widget',content[i].name,'at',i)
-- end -- end
-- @usage
-- -- Note: layout names can collide with method names. Because of that you can't use a layout name such as "insert":
-- local content = ui.content {
-- { name = 'insert '}
-- }
-- content.insert.content = ui.content {} -- fails here, content.insert is a function!
--- ---
-- Puts the layout at given index by shifting all the elements after it -- Puts the layout at given index by shifting all the elements after it