From 68963538ae2013943bd3dad3e79cc71259a852a4 Mon Sep 17 00:00:00 2001 From: uramer Date: Thu, 18 Nov 2021 15:19:54 +0000 Subject: [PATCH] Lua UI API --- apps/openmw/mwgui/windowmanagerimp.cpp | 3 + apps/openmw/mwlua/uibindings.cpp | 169 +++++++++- apps/openmw_test_suite/CMakeLists.txt | 2 + .../openmw_test_suite/lua/test_ui_content.cpp | 97 ++++++ components/CMakeLists.txt | 5 + components/lua_ui/content.cpp | 106 ++++++ components/lua_ui/content.hpp | 41 +++ components/lua_ui/element.cpp | 151 +++++++++ components/lua_ui/element.hpp | 31 ++ components/lua_ui/text.cpp | 40 +++ components/lua_ui/text.hpp | 26 ++ components/lua_ui/textedit.cpp | 19 ++ components/lua_ui/textedit.hpp | 19 ++ components/lua_ui/widget.cpp | 307 ++++++++++++++++++ components/lua_ui/widget.hpp | 91 ++++++ components/lua_ui/widgetlist.cpp | 31 ++ components/lua_ui/widgetlist.hpp | 14 + components/lua_ui/window.cpp | 94 ++++++ components/lua_ui/window.hpp | 33 ++ docs/source/generate_luadoc.sh | 23 +- docs/source/reference/lua-scripting/api.rst | 5 +- .../reference/lua-scripting/overview.rst | 4 +- .../lua-scripting/user_interface.rst | 151 +++++++++ .../lua-scripting/widgets/widget.rst | 77 +++++ files/lua_api/openmw/ui.lua | 108 +++++- 25 files changed, 1633 insertions(+), 14 deletions(-) create mode 100644 apps/openmw_test_suite/lua/test_ui_content.cpp create mode 100644 components/lua_ui/content.cpp create mode 100644 components/lua_ui/content.hpp create mode 100644 components/lua_ui/element.cpp create mode 100644 components/lua_ui/element.hpp create mode 100644 components/lua_ui/text.cpp create mode 100644 components/lua_ui/text.hpp create mode 100644 components/lua_ui/textedit.cpp create mode 100644 components/lua_ui/textedit.hpp create mode 100644 components/lua_ui/widget.cpp create mode 100644 components/lua_ui/widget.hpp create mode 100644 components/lua_ui/widgetlist.cpp create mode 100644 components/lua_ui/widgetlist.hpp create mode 100644 components/lua_ui/window.cpp create mode 100644 components/lua_ui/window.hpp create mode 100644 docs/source/reference/lua-scripting/user_interface.rst create mode 100644 docs/source/reference/lua-scripting/widgets/widget.rst diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 6eeb2d3654..a935d7f900 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -53,6 +53,8 @@ #include #include +#include + #include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -220,6 +222,7 @@ namespace MWGui ItemWidget::registerComponents(); SpellView::registerComponents(); Gui::registerAllWidgets(); + LuaUi::registerAllWidgets(); MyGUI::FactoryManager::getInstance().registerFactory("Controller"); diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 4fae84cd40..7008a0caea 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -1,17 +1,182 @@ -#include "luabindings.hpp" +#include +#include +#include +#include "context.hpp" +#include "actions.hpp" #include "luamanagerimp.hpp" namespace MWLua { + namespace + { + + class UiAction final : public Action + { + public: + enum Type + { + CREATE = 0, + UPDATE, + DESTROY, + }; + + UiAction(Type type, std::shared_ptr element, LuaUtil::LuaState* state) + : Action(state) + , mType{ type } + , mElement{ std::move(element) } + {} + + void apply(WorldView&) const override + { + try { + switch (mType) + { + case CREATE: + mElement->create(); + break; + case UPDATE: + mElement->update(); + break; + case DESTROY: + mElement->destroy(); + break; + } + } + catch (std::exception& e) + { + // prevent any actions on a potentially corrupted widget + mElement->mRoot = nullptr; + throw; + } + } + + std::string toString() const override + { + std::string result; + switch (mType) + { + case CREATE: + result += "Create"; + break; + case UPDATE: + result += "Update"; + break; + case DESTROY: + result += "Destroy"; + break; + } + result += " UI"; + return result; + } + + private: + Type mType; + std::shared_ptr mElement; + }; + + // Lua arrays index from 1 + inline size_t fromLuaIndex(size_t i) { return i - 1; } + inline size_t toLuaIndex(size_t i) { return i + 1; } + } sol::table initUserInterfacePackage(const Context& context) { - sol::table api(context.mLua->sol(), sol::create); + auto uiContent = context.mLua->sol().new_usertype("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 index = content.indexOf(table); + if (index < content.size()) + return toLuaIndex(index); + else + return sol::nullopt; + }; + + auto element = context.mLua->sol().new_usertype("Element"); + element["layout"] = sol::property( + [](LuaUi::Element& element) + { + return element.mLayout; + }, + [](LuaUi::Element& element, const sol::table& layout) + { + element.mLayout = layout; + } + ); + element["update"] = [context](const std::shared_ptr& element) + { + if (element->mDestroy || element->mUpdate) + return; + element->mUpdate = true; + context.mLuaManager->addAction(std::make_unique(UiAction::UPDATE, element, context.mLua)); + }; + element["destroy"] = [context](const std::shared_ptr& element) + { + if (element->mDestroy) + return; + element->mDestroy = true; + context.mLuaManager->addAction(std::make_unique(UiAction::DESTROY, element, context.mLua)); + }; + + sol::table api = context.mLua->newTable(); api["showMessage"] = [luaManager=context.mLuaManager](std::string_view message) { luaManager->addUIMessage(message); }; + api["content"] = [](const sol::table& table) + { + return LuaUi::Content(table); + }; + api["create"] = [context](const sol::table& layout) + { + auto element = std::make_shared(layout); + context.mLuaManager->addAction(std::make_unique(UiAction::CREATE, element, context.mLua)); + return element; + }; + + sol::table typeTable = context.mLua->newTable(); + for (const auto& it : LuaUi::widgetTypeToName()) + typeTable.set(it.second, it.first); + api["TYPE"] = LuaUtil::makeReadOnly(typeTable); + return LuaUtil::makeReadOnly(api); } diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index a4e9f8323c..29564ef191 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -24,6 +24,8 @@ if (GTEST_FOUND AND GMOCK_FOUND) lua/test_querypackage.cpp lua/test_configuration.cpp + lua/test_ui_content.cpp + misc/test_stringops.cpp misc/test_endianness.cpp misc/test_resourcehelpers.cpp diff --git a/apps/openmw_test_suite/lua/test_ui_content.cpp b/apps/openmw_test_suite/lua/test_ui_content.cpp new file mode 100644 index 0000000000..f478c618dc --- /dev/null +++ b/apps/openmw_test_suite/lua/test_ui_content.cpp @@ -0,0 +1,97 @@ +#include +#include + +#include + +namespace +{ + using namespace testing; + + sol::state state; + + sol::table makeTable() + { + return sol::table(state, sol::create); + } + + sol::table makeTable(std::string name) + { + auto result = makeTable(); + result["name"] = name; + return result; + } + + TEST(LuaUiContentTest, Create) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::Content content(table); + EXPECT_EQ(content.size(), 3); + } + + TEST(LuaUiContentTest, CreateWithHole) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table[4] = makeTable(); + EXPECT_ANY_THROW(LuaUi::Content content(table)); + } + + TEST(LuaUiContentTest, WrongType) + { + auto table = makeTable(); + table.add(makeTable()); + table.add("a"); + table.add(makeTable()); + EXPECT_ANY_THROW(LuaUi::Content content(table)); + } + + TEST(LuaUiContentTest, NameAccess) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable("a")); + LuaUi::Content content(table); + EXPECT_NO_THROW(content.at("a")); + content.remove("a"); + content.assign(content.size(), makeTable("b")); + content.assign("b", makeTable()); + EXPECT_ANY_THROW(content.at("b")); + EXPECT_EQ(content.size(), 2); + content.assign(content.size(), makeTable("c")); + content.assign(content.size(), makeTable("c")); + content.remove("c"); + EXPECT_ANY_THROW(content.at("c")); + } + + TEST(LuaUiContentTest, IndexOf) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::Content content(table); + auto child = makeTable(); + content.assign(2, child); + EXPECT_EQ(content.indexOf(child), 2); + EXPECT_EQ(content.indexOf(makeTable()), content.size()); + } + + TEST(LuaUiContentTest, BoundsChecks) + { + auto table = makeTable(); + LuaUi::Content content(table); + EXPECT_ANY_THROW(content.at(0)); + content.assign(content.size(), makeTable()); + content.assign(content.size(), makeTable()); + content.assign(content.size(), makeTable()); + EXPECT_ANY_THROW(content.at(3)); + EXPECT_ANY_THROW(content.remove(3)); + EXPECT_NO_THROW(content.remove(1)); + EXPECT_NO_THROW(content.at(1)); + EXPECT_EQ(content.size(), 2); + } +} diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 71a96716ec..bf073a0e78 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -160,6 +160,11 @@ add_component_dir (fallback add_component_dir (queries query luabindings ) + +add_component_dir (lua_ui + widget widgetlist element content text textedit window + ) + if(WIN32) add_component_dir (crashcatcher diff --git a/components/lua_ui/content.cpp b/components/lua_ui/content.cpp new file mode 100644 index 0000000000..6f9cf61f2f --- /dev/null +++ b/components/lua_ui/content.cpp @@ -0,0 +1,106 @@ +#include "content.hpp" + +namespace LuaUi +{ + Content::Content(const sol::table& table) + { + size_t size = table.size(); + for (size_t index = 0; index < size; ++index) + { + sol::object value = table.get(index + 1); + if (value.is()) + assign(index, value.as()); + else + throw std::logic_error("UI Content children must all be tables."); + } + } + + void Content::assign(size_t index, const sol::table& table) + { + if (mOrdered.size() < index) + throw std::logic_error("Can't have gaps in UI Content."); + if (index == mOrdered.size()) + mOrdered.push_back(table); + else + { + sol::optional oldName = mOrdered[index]["name"]; + if (oldName.has_value()) + mNamed.erase(oldName.value()); + mOrdered[index] = table; + } + sol::optional 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) + { + size_t size = mOrdered.size(); + if (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 < size; ++i) + { + sol::optional 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 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(); + } +} diff --git a/components/lua_ui/content.hpp b/components/lua_ui/content.hpp new file mode 100644 index 0000000000..e970744b3d --- /dev/null +++ b/components/lua_ui/content.hpp @@ -0,0 +1,41 @@ +#ifndef COMPONENTS_LUAUI_CONTENT +#define COMPONENTS_LUAUI_CONTENT + +#include +#include + +#include + +namespace LuaUi +{ + class Content + { + public: + using iterator = std::vector::iterator; + + Content() {} + + // expects a Lua array - a table with keys from 1 to n without any nil values in between + // any other keys are ignored + explicit Content(const sol::table&); + + size_t size() const { return mOrdered.size(); } + + 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); + + 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: + std::map> mNamed; + std::vector mOrdered; + }; + +} + +#endif // COMPONENTS_LUAUI_CONTENT diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp new file mode 100644 index 0000000000..258da5269f --- /dev/null +++ b/components/lua_ui/element.cpp @@ -0,0 +1,151 @@ +#include "element.hpp" + +#include + +#include "content.hpp" +#include "widgetlist.hpp" + +namespace LuaUi +{ + + std::string widgetType(const sol::table& layout) + { + return layout.get_or("type", std::string("LuaWidget")); + } + + Content content(const sol::table& layout) + { + auto optional = layout.get>("content"); + if (optional.has_value()) + return optional.value(); + else + return Content(); + } + + void setProperties(LuaUi::WidgetExtension* ext, const sol::table& layout) + { + auto props = layout.get>("props"); + if (props.has_value()) + { + props.value().for_each([ext](const sol::object& key, const sol::object& value) + { + if (key.is()) + ext->setProperty(key.as(), value); + else + Log(Debug::Warning) << "UI property key must be a string"; + }); + ext->updateCoord(); + } + } + + void setEventCallbacks(LuaUi::WidgetExtension* ext, const sol::table& layout) + { + ext->clearCallbacks(); + auto events = layout.get>("events"); + if (events.has_value()) + { + events.value().for_each([ext](const sol::object& name, const sol::object& callback) + { + if (name.is() && callback.is()) + ext->setCallback(name.as(), callback.as()); + else if (!name.is()) + Log(Debug::Warning) << "UI event key must be a string"; + else if (!callback.is()) + Log(Debug::Warning) << "UI event handler for key \"" << name.as() + << "\" must be an openmw.async.callback"; + }); + } + } + + LuaUi::WidgetExtension* createWidget(const sol::table& layout, LuaUi::WidgetExtension* parent) + { + std::string type = widgetType(layout); + std::string skin = layout.get_or("skin", std::string()); + std::string layer = layout.get_or("layer", std::string("Windows")); + std::string name = layout.get_or("name", std::string()); + + static auto widgetTypeMap = widgetTypeToName(); + if (widgetTypeMap.find(type) == widgetTypeMap.end()) + throw std::logic_error(std::string("Invalid widget type ") += type); + + MyGUI::Widget* widget = MyGUI::Gui::getInstancePtr()->createWidgetT( + type, skin, + MyGUI::IntCoord(), MyGUI::Align::Default, + layer, name); + + LuaUi::WidgetExtension* ext = dynamic_cast(widget); + if (!ext) + throw std::runtime_error("Invalid widget!"); + + ext->create(layout.lua_state(), widget); + if (parent != nullptr) + widget->attachToWidget(parent->widget()); + + setEventCallbacks(ext, layout); + setProperties(ext, layout); + + Content cont = content(layout); + for (size_t i = 0; i < cont.size(); i++) + ext->addChild(createWidget(cont.at(i), ext)); + + return ext; + } + + void destroyWidget(LuaUi::WidgetExtension* ext) + { + ext->destroy(); + MyGUI::Gui::getInstancePtr()->destroyWidget(ext->widget()); + } + + void updateWidget(const sol::table& layout, LuaUi::WidgetExtension* ext) + { + setEventCallbacks(ext, layout); + setProperties(ext, layout); + + Content newContent = content(layout); + + size_t oldSize = ext->childCount(); + size_t newSize = newContent.size(); + size_t minSize = std::min(oldSize, newSize); + for (size_t i = 0; i < minSize; i++) + { + LuaUi::WidgetExtension* oldWidget = ext->childAt(i); + sol::table newChild = newContent.at(i); + + if (oldWidget->widget()->getTypeName() != widgetType(newChild)) + { + destroyWidget(oldWidget); + ext->assignChild(i, createWidget(newChild, ext)); + } + else + updateWidget(newChild, oldWidget); + } + + for (size_t i = minSize; i < oldSize; i++) + destroyWidget(ext->eraseChild(i)); + + for (size_t i = minSize; i < newSize; i++) + ext->addChild(createWidget(newContent.at(i), ext)); + } + + void Element::create() + { + assert(!mRoot); + if (!mRoot) + mRoot = createWidget(mLayout, nullptr); + } + + void Element::update() + { + if (mRoot && mUpdate) + updateWidget(mLayout, mRoot); + mUpdate = false; + } + + void Element::destroy() + { + if (mRoot) + destroyWidget(mRoot); + mRoot = nullptr; + } +} diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp new file mode 100644 index 0000000000..10e10d8960 --- /dev/null +++ b/components/lua_ui/element.hpp @@ -0,0 +1,31 @@ +#ifndef OPENMW_LUAUI_ELEMENT +#define OPENMW_LUAUI_ELEMENT + +#include "widget.hpp" + +namespace LuaUi +{ + struct Element + { + Element(sol::table layout) + : mRoot{ nullptr } + , mLayout{ layout } + , mUpdate{ false } + , mDestroy{ false } + { + } + + LuaUi::WidgetExtension* mRoot; + sol::table mLayout; + bool mUpdate; + bool mDestroy; + + void create(); + + void update(); + + void destroy(); + }; +} + +#endif // !OPENMW_LUAUI_ELEMENT diff --git a/components/lua_ui/text.cpp b/components/lua_ui/text.cpp new file mode 100644 index 0000000000..241af981b0 --- /dev/null +++ b/components/lua_ui/text.cpp @@ -0,0 +1,40 @@ + +#include "text.hpp" + +namespace LuaUi +{ + void LuaText::initialize() + { + WidgetExtension::initialize(); + mAutoSized = true; + } + + bool LuaText::setPropertyRaw(std::string_view name, sol::object value) + { + if (name == "caption") + { + if (!value.is()) + return false; + setCaption(value.as()); + } + else if (name == "autoSize") + { + if (!value.is()) + return false; + mAutoSized = value.as(); + } + else + { + return WidgetExtension::setPropertyRaw(name, value); + } + return true; + } + + MyGUI::IntSize LuaText::calculateSize() + { + if (mAutoSized) + return getTextSize(); + else + return WidgetExtension::calculateSize(); + } +} diff --git a/components/lua_ui/text.hpp b/components/lua_ui/text.hpp new file mode 100644 index 0000000000..08de275d23 --- /dev/null +++ b/components/lua_ui/text.hpp @@ -0,0 +1,26 @@ +#ifndef OPENMW_LUAUI_TEXT +#define OPENMW_LUAUI_TEXT + +#include + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaText : public MyGUI::TextBox, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaText) + + public: + virtual void initialize() override; + + private: + bool mAutoSized; + + protected: + virtual MyGUI::IntSize calculateSize() override; + bool setPropertyRaw(std::string_view name, sol::object value) override; + }; +} + +#endif // OPENMW_LUAUI_TEXT diff --git a/components/lua_ui/textedit.cpp b/components/lua_ui/textedit.cpp new file mode 100644 index 0000000000..2324a770c2 --- /dev/null +++ b/components/lua_ui/textedit.cpp @@ -0,0 +1,19 @@ +#include "textedit.hpp" + +namespace LuaUi +{ + bool LuaTextEdit::setPropertyRaw(std::string_view name, sol::object value) + { + if (name == "caption") + { + if (!value.is()) + return false; + setCaption(value.as()); + } + else + { + return WidgetExtension::setPropertyRaw(name, value); + } + return true; + } +} diff --git a/components/lua_ui/textedit.hpp b/components/lua_ui/textedit.hpp new file mode 100644 index 0000000000..7d3291692c --- /dev/null +++ b/components/lua_ui/textedit.hpp @@ -0,0 +1,19 @@ +#ifndef OPENMW_LUAUI_TEXTEDIT +#define OPENMW_LUAUI_TEXTEDIT + +#include + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaTextEdit : public MyGUI::EditBox, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaTextEdit) + + protected: + bool setPropertyRaw(std::string_view name, sol::object value) override; + }; +} + +#endif // OPENMW_LUAUI_TEXTEDIT diff --git a/components/lua_ui/widget.cpp b/components/lua_ui/widget.cpp new file mode 100644 index 0000000000..5cbf1f318d --- /dev/null +++ b/components/lua_ui/widget.cpp @@ -0,0 +1,307 @@ +#include "widget.hpp" + +#include + +#include "text.hpp" +#include "textedit.hpp" +#include "window.hpp" + +namespace LuaUi +{ + void WidgetExtension::triggerEvent(std::string_view name, const sol::object& argument = sol::nil) const + { + auto it = mCallbacks.find(name); + if (it != mCallbacks.end()) + it->second(argument); + } + + void WidgetExtension::create(lua_State* lua, MyGUI::Widget* self) + { + mLua = lua; + mWidget = self; + + mWidget->eventChangeCoord += MyGUI::newDelegate(this, &WidgetExtension::updateChildrenCoord); + + initialize(); + } + + void WidgetExtension::initialize() + { + mAbsoluteCoord = MyGUI::IntCoord(); + mRelativeCoord = MyGUI::FloatCoord(); + mAnchor = MyGUI::FloatSize(); + mForcedCoord = MyGUI::IntCoord(); + + // \todo might be more efficient to only register these if there are Lua callbacks + mWidget->eventKeyButtonPressed += MyGUI::newDelegate(this, &WidgetExtension::keyPress); + mWidget->eventKeyButtonReleased += MyGUI::newDelegate(this, &WidgetExtension::keyRelease); + mWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &WidgetExtension::mouseClick); + mWidget->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &WidgetExtension::mouseDoubleClick); + mWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &WidgetExtension::mousePress); + mWidget->eventMouseButtonReleased += MyGUI::newDelegate(this, &WidgetExtension::mouseRelease); + mWidget->eventMouseMove += MyGUI::newDelegate(this, &WidgetExtension::mouseMove); + mWidget->eventMouseDrag += MyGUI::newDelegate(this, &WidgetExtension::mouseDrag); + + mWidget->eventMouseSetFocus += MyGUI::newDelegate(this, &WidgetExtension::focusGain); + mWidget->eventMouseLostFocus += MyGUI::newDelegate(this, &WidgetExtension::focusLoss); + mWidget->eventKeySetFocus += MyGUI::newDelegate(this, &WidgetExtension::focusGain); + mWidget->eventKeyLostFocus += MyGUI::newDelegate(this, &WidgetExtension::focusLoss); + } + + void WidgetExtension::destroy() + { + clearCallbacks(); + deinitialize(); + + for (WidgetExtension* child : mContent) + child->destroy(); + } + + void WidgetExtension::deinitialize() + { + mWidget->eventKeyButtonPressed.clear(); + mWidget->eventKeyButtonReleased.clear(); + mWidget->eventMouseButtonClick.clear(); + mWidget->eventMouseButtonDoubleClick.clear(); + mWidget->eventMouseButtonPressed.clear(); + mWidget->eventMouseButtonReleased.clear(); + mWidget->eventMouseMove.clear(); + mWidget->eventMouseDrag.m_event.clear(); + + mWidget->eventMouseSetFocus.clear(); + mWidget->eventMouseLostFocus.clear(); + mWidget->eventKeySetFocus.clear(); + mWidget->eventKeyLostFocus.clear(); + } + + sol::table WidgetExtension::makeTable() const + { + return sol::table(mLua, sol::create); + } + + sol::object WidgetExtension::keyEvent(MyGUI::KeyCode code) const + { + SDL_Keysym keySym; + // MyGUI key codes are not one to one with SDL key codes + // \todo refactor sdlmappings.cpp to map this back to SDL correctly + keySym.sym = static_cast(code.getValue()); + keySym.scancode = SDL_GetScancodeFromKey(keySym.sym); + keySym.mod = SDL_GetModState(); + return sol::make_object(mLua, keySym); + } + + sol::object WidgetExtension::mouseEvent(int left, int top, MyGUI::MouseButton button = MyGUI::MouseButton::None) const + { + auto position = osg::Vec2f(left, top); + auto absolutePosition = mWidget->getAbsolutePosition(); + auto offset = position - osg::Vec2f(absolutePosition.left, absolutePosition.top); + sol::table table = makeTable(); + table["position"] = position; + table["offset"] = offset; + // \todo refactor sdlmappings.cpp to map this back to SDL properly + table["button"] = button.getValue() + 1; + return table; + } + + void WidgetExtension::addChild(WidgetExtension* ext) + { + mContent.push_back(ext); + } + + WidgetExtension* WidgetExtension::childAt(size_t index) const + { + return mContent.at(index); + } + + void WidgetExtension::assignChild(size_t index, WidgetExtension* ext) + { + if (mContent.size() <= index) + throw std::logic_error("Invalid widget child index"); + mContent[index] = ext; + } + + WidgetExtension* WidgetExtension::eraseChild(size_t index) + { + if (mContent.size() <= index) + throw std::logic_error("Invalid widget child index"); + auto it = mContent.begin() + index; + WidgetExtension* ext = *it; + mContent.erase(it); + return ext; + } + + void WidgetExtension::setCallback(const std::string& name, const LuaUtil::Callback& callback) + { + mCallbacks[name] = callback; + } + + void WidgetExtension::clearCallbacks() + { + mCallbacks.clear(); + } + + void WidgetExtension::setProperty(std::string_view name, sol::object value) + { + if (!setPropertyRaw(name, value)) + Log(Debug::Error) << "Invalid value of property " << name + << ": " << LuaUtil::toString(value); + } + + MyGUI::IntCoord WidgetExtension::forcedOffset() + { + return mForcedCoord; + } + + void WidgetExtension::setForcedOffset(const MyGUI::IntCoord& offset) + { + mForcedCoord = offset; + } + + void WidgetExtension::updateCoord() + { + mWidget->setCoord(calculateCoord()); + } + + bool WidgetExtension::setPropertyRaw(std::string_view name, sol::object value) + { + if (name == "position") + { + if (!value.is()) + return false; + auto v = value.as(); + mAbsoluteCoord.left = v.x(); + mAbsoluteCoord.top = v.y(); + } + else if (name == "size") + { + if (!value.is()) + return false; + auto v = value.as(); + mAbsoluteCoord.width = v.x(); + mAbsoluteCoord.height = v.y(); + } + else if (name == "relativePosition") + { + if (!value.is()) + return false; + auto v = value.as(); + mRelativeCoord.left = v.x(); + mRelativeCoord.top = v.y(); + } + else if (name == "relativeSize") + { + if (!value.is()) + return false; + auto v = value.as(); + mRelativeCoord.width = v.x(); + mRelativeCoord.height = v.y(); + } + else if (name == "anchor") + { + if (!value.is()) + return false; + auto v = value.as(); + mAnchor.width = v.x(); + mAnchor.height = v.y(); + } + else if (name == "visible") + { + if (!value.is()) + return false; + mWidget->setVisible(value.as()); + } + return true; + } + + void WidgetExtension::updateChildrenCoord(MyGUI::Widget* _widget) + { + for (auto& child : mContent) + child->updateCoord(); + } + + MyGUI::IntSize WidgetExtension::calculateSize() + { + const MyGUI::IntSize& parentSize = mWidget->getParentSize(); + MyGUI::IntSize newSize; + newSize = mAbsoluteCoord.size() + mForcedCoord.size(); + newSize.width += mRelativeCoord.width * parentSize.width; + newSize.height += mRelativeCoord.height * parentSize.height; + return newSize; + } + + MyGUI::IntPoint WidgetExtension::calculatePosition(const MyGUI::IntSize& size) + { + const MyGUI::IntSize& parentSize = mWidget->getParentSize(); + MyGUI::IntPoint newPosition; + newPosition = mAbsoluteCoord.point() + mForcedCoord.point(); + newPosition.left += mRelativeCoord.left * parentSize.width - mAnchor.width * size.width; + newPosition.top += mRelativeCoord.top * parentSize.height - mAnchor.height * size.height; + return newPosition; + } + + MyGUI::IntCoord WidgetExtension::calculateCoord() + { + MyGUI::IntCoord newCoord; + newCoord = calculateSize(); + newCoord = calculatePosition(newCoord.size()); + return newCoord; + } + + void WidgetExtension::keyPress(MyGUI::Widget*, MyGUI::KeyCode code, MyGUI::Char ch) + { + if (code == MyGUI::KeyCode::None) + { + // \todo decide how to handle unicode strings in Lua + MyGUI::UString uString; + uString.push_back(static_cast(ch)); + triggerEvent("textInput", sol::make_object(mLua, uString.asUTF8())); + } + else + triggerEvent("keyPress", keyEvent(code)); + } + + void WidgetExtension::keyRelease(MyGUI::Widget*, MyGUI::KeyCode code) + { + triggerEvent("keyRelease", keyEvent(code)); + } + + void WidgetExtension::mouseMove(MyGUI::Widget*, int left, int top) + { + triggerEvent("mouseMove", mouseEvent(left, top)); + } + + void WidgetExtension::mouseDrag(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) + { + triggerEvent("mouseMove", mouseEvent(left, top, button)); + } + + void WidgetExtension::mouseClick(MyGUI::Widget* _widget) + { + triggerEvent("mouseClick"); + } + + void WidgetExtension::mouseDoubleClick(MyGUI::Widget* _widget) + { + triggerEvent("mouseDoubleClick"); + } + + void WidgetExtension::mousePress(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) + { + triggerEvent("mousePress", mouseEvent(left, top, button)); + } + + void WidgetExtension::mouseRelease(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) + { + triggerEvent("mouseRelease", mouseEvent(left, top, button)); + } + + void WidgetExtension::focusGain(MyGUI::Widget*, MyGUI::Widget*) + { + triggerEvent("focusGain"); + } + + void WidgetExtension::focusLoss(MyGUI::Widget*, MyGUI::Widget*) + { + triggerEvent("focusLoss"); + } +} diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp new file mode 100644 index 0000000000..fe1ad46716 --- /dev/null +++ b/components/lua_ui/widget.hpp @@ -0,0 +1,91 @@ +#ifndef OPENMW_LUAUI_WIDGET +#define OPENMW_LUAUI_WIDGET + +#include + +#include +#include +#include + +#include + +namespace LuaUi +{ + /* + * extends MyGUI::Widget and its child classes + * memory ownership is controlled by MyGUI + * it is important not to call any WidgetExtension methods after destroying the MyGUI::Widget + */ + class WidgetExtension + { + public: + // must be called after creating the underlying MyGUI::Widget + void create(lua_State* lua, MyGUI::Widget* self); + // must be called after before destroying the underlying MyGUI::Widget + void destroy(); + + void addChild(WidgetExtension* ext); + WidgetExtension* childAt(size_t index) const; + void assignChild(size_t index, WidgetExtension* ext); + WidgetExtension* eraseChild(size_t index); + size_t childCount() const { return mContent.size(); } + + MyGUI::Widget* widget() const { return mWidget; } + + void setCallback(const std::string&, const LuaUtil::Callback&); + void clearCallbacks(); + + void setProperty(std::string_view, sol::object value); + + MyGUI::IntCoord forcedOffset(); + void setForcedOffset(const MyGUI::IntCoord& offset); + void updateCoord(); + + protected: + ~WidgetExtension() {} + sol::table makeTable() const; + sol::object keyEvent(MyGUI::KeyCode) const; + sol::object mouseEvent(int left, int top, MyGUI::MouseButton button) const; + virtual bool setPropertyRaw(std::string_view name, sol::object value); + virtual void initialize(); + virtual void deinitialize(); + virtual MyGUI::IntSize calculateSize(); + virtual MyGUI::IntPoint calculatePosition(const MyGUI::IntSize& size); + MyGUI::IntCoord calculateCoord(); + + void triggerEvent(std::string_view name, const sol::object& argument) const; + + MyGUI::IntCoord mForcedCoord; + MyGUI::IntCoord mAbsoluteCoord; + MyGUI::FloatCoord mRelativeCoord; + MyGUI::FloatSize mAnchor; + + private: + // use lua_State* instead of sol::state_view because MyGUI requires a default constructor + lua_State* mLua; + MyGUI::Widget* mWidget; + + std::vector mContent; + std::map> mCallbacks; + + void updateChildrenCoord(MyGUI::Widget*); + + void keyPress(MyGUI::Widget*, MyGUI::KeyCode, MyGUI::Char); + void keyRelease(MyGUI::Widget*, MyGUI::KeyCode); + void mouseMove(MyGUI::Widget*, int, int); + void mouseDrag(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void mouseClick(MyGUI::Widget*); + void mouseDoubleClick(MyGUI::Widget*); + void mousePress(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void mouseRelease(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void focusGain(MyGUI::Widget*, MyGUI::Widget*); + void focusLoss(MyGUI::Widget*, MyGUI::Widget*); + }; + + class LuaWidget : public MyGUI::Widget, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaWidget) + }; +} + +#endif // !OPENMW_LUAUI_WIDGET diff --git a/components/lua_ui/widgetlist.cpp b/components/lua_ui/widgetlist.cpp new file mode 100644 index 0000000000..c2a9bef990 --- /dev/null +++ b/components/lua_ui/widgetlist.cpp @@ -0,0 +1,31 @@ +#include "widgetlist.hpp" + +#include + +#include "widget.hpp" +#include "text.hpp" +#include "textedit.hpp" +#include "window.hpp" + +namespace LuaUi +{ + + void registerAllWidgets() + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + } + + const std::unordered_map& widgetTypeToName() + { + static std::unordered_map types{ + { "LuaWidget", "Widget" }, + { "LuaText", "Text" }, + { "LuaTextEdit", "TextEdit" }, + { "LuaWindow", "Window" }, + }; + return types; + } +} diff --git a/components/lua_ui/widgetlist.hpp b/components/lua_ui/widgetlist.hpp new file mode 100644 index 0000000000..ff033fb6ca --- /dev/null +++ b/components/lua_ui/widgetlist.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_LUAUI_WIDGETLIST +#define OPENMW_LUAUI_WIDGETLIST + +#include +#include + +namespace LuaUi +{ + void registerAllWidgets(); + + const std::unordered_map& widgetTypeToName(); +} + +#endif // OPENMW_LUAUI_WIDGETLIST diff --git a/components/lua_ui/window.cpp b/components/lua_ui/window.cpp new file mode 100644 index 0000000000..874ccb9fa4 --- /dev/null +++ b/components/lua_ui/window.cpp @@ -0,0 +1,94 @@ +#include "window.hpp" + +#include + +namespace LuaUi +{ + void LuaWindow::initialize() + { + WidgetExtension::initialize(); + + assignWidget(mCaption, "Caption"); + if (mCaption) + { + mCaption->eventMouseButtonPressed += MyGUI::newDelegate(this, &LuaWindow::notifyMousePress); + mCaption->eventMouseDrag += MyGUI::newDelegate(this, &LuaWindow::notifyMouseDrag); + } + for (auto w : getSkinWidgetsByName("Action")) + { + w->eventMouseButtonPressed += MyGUI::newDelegate(this, &LuaWindow::notifyMousePress); + w->eventMouseDrag += MyGUI::newDelegate(this, &LuaWindow::notifyMouseDrag); + } + } + + void LuaWindow::deinitialize() + { + WidgetExtension::deinitialize(); + + if (mCaption) + { + mCaption->eventMouseButtonPressed.clear(); + mCaption->eventMouseDrag.m_event.clear(); + } + for (auto w : getSkinWidgetsByName("Action")) + { + w->eventMouseButtonPressed.clear(); + w->eventMouseDrag.m_event.clear(); + } + } + + bool LuaWindow::setPropertyRaw(std::string_view name, sol::object value) + { + if (name == "caption") + { + if (!value.is()) + return false; + if (mCaption) + mCaption->setCaption(value.as()); + } + else + { + return WidgetExtension::setPropertyRaw(name, value); + } + return true; + } + + void LuaWindow::notifyMousePress(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id) + { + if (id != MyGUI::MouseButton::Left) + return; + + mPreviousMouse.left = left; + mPreviousMouse.top = top; + + if (sender->isUserString("Scale")) + mChangeScale = MyGUI::IntCoord::parse(sender->getUserString("Scale")); + else + mChangeScale = MyGUI::IntCoord(1, 1, 0, 0); + } + + void LuaWindow::notifyMouseDrag(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id) + { + if (id != MyGUI::MouseButton::Left) + return; + + MyGUI::IntCoord change = mChangeScale; + change.left *= (left - mPreviousMouse.left); + change.top *= (top - mPreviousMouse.top); + change.width *= (left - mPreviousMouse.left); + change.height *= (top - mPreviousMouse.top); + + setForcedOffset(forcedOffset() + change.size()); + MyGUI::IntPoint positionOffset = change.point() + getPosition() - calculateCoord().point(); + setForcedOffset(forcedOffset() + positionOffset); + updateCoord(); + + mPreviousMouse.left = left; + mPreviousMouse.top = top; + + sol::table table = makeTable(); + table["position"] = osg::Vec2f(mCoord.left, mCoord.top); + table["size"] = osg::Vec2f(mCoord.width, mCoord.height); + triggerEvent("windowDrag", table); + } +} diff --git a/components/lua_ui/window.hpp b/components/lua_ui/window.hpp new file mode 100644 index 0000000000..d92bf8dcbf --- /dev/null +++ b/components/lua_ui/window.hpp @@ -0,0 +1,33 @@ +#ifndef OPENMW_LUAUI_WINDOW +#define OPENMW_LUAUI_WINDOW + +#include + +#include + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaWindow : public MyGUI::Widget, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaWindow) + + private: + // \todo replace with LuaText when skins are properly implemented + MyGUI::TextBox* mCaption; + MyGUI::IntPoint mPreviousMouse; + MyGUI::IntCoord mChangeScale; + + protected: + virtual void initialize() override; + virtual void deinitialize() override; + + bool setPropertyRaw(std::string_view name, sol::object value) override; + + void notifyMousePress(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void notifyMouseDrag(MyGUI::Widget*, int, int, MyGUI::MouseButton); + }; +} + +#endif // OPENMW_LUAUI_WINDOW diff --git a/docs/source/generate_luadoc.sh b/docs/source/generate_luadoc.sh index 7a238eca5a..1af9f0e0f7 100755 --- a/docs/source/generate_luadoc.sh +++ b/docs/source/generate_luadoc.sh @@ -8,6 +8,18 @@ # luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec # luarocks --local install openmwluadocumentor-0.1.1-1.src.rock +# How to install on Windows: + +# install LuaRocks (heavily recommended to use the standalone package) +# https://github.com/luarocks/luarocks/wiki/Installation-instructions-for-Windows +# git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git +# cd openmw-luadocumentor/luarocks +# open "Developer Command Prompt for VS <2017/2019>" in this directory and run: +# luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec +# luarocks --local install openmwluadocumentor-0.1.1-1.src.rock +# open "Git Bash" in the same directory and run script: +# ./generate_luadoc.sh + if [ -f /.dockerenv ]; then # We are inside readthedocs pipeline echo "Install lua 5.1" @@ -32,7 +44,6 @@ if [ -f /.dockerenv ]; then cd ~ git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git cd openmw-luadocumentor/luarocks - luarocks --local install checks luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec luarocks --local install openmwluadocumentor-0.1.1-1.src.rock fi @@ -40,12 +51,18 @@ fi DOCS_SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" FILES_DIR=$DOCS_SOURCE_DIR/../../files OUTPUT_DIR=$DOCS_SOURCE_DIR/reference/lua-scripting/generated_html +DOCUMENTOR_PATH=~/.luarocks/bin/openmwluadocumentor + +if [ ! -x $DOCUMENTOR_PATH ]; then + # running on Windows? + DOCUMENTOR_PATH="$APPDATA/LuaRocks/bin/openmwluadocumentor.bat" +fi rm -f $OUTPUT_DIR/*.html cd $FILES_DIR/lua_api -~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw/*lua +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw/*lua cd $FILES_DIR/builtin_scripts -~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw_aux/*lua +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw_aux/*lua diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 5075f8a5fe..6398a19be8 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -6,6 +6,7 @@ Lua API reference :hidden: engine_handlers + user_interface openmw_util openmw_settings openmw_core @@ -20,6 +21,7 @@ Lua API reference - :ref:`Engine handlers reference` +- :ref:`User interface reference ` - `Game object reference `_ - `Cell reference `_ @@ -56,7 +58,7 @@ Player scripts are local scripts that are attached to a player. +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.input ` | by player scripts | | User input | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.ui ` | by player scripts | | Controls user interface | +|:ref:`openmw.ui ` | by player scripts | | Controls :ref:`user interface ` | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |openmw.camera | by player scripts | | Controls camera (not implemented) | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ @@ -71,4 +73,3 @@ Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can overrid +=========================================================+====================+===============================================================+ |:ref:`openmw_aux.util ` | everywhere | | Miscellaneous utils | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ - diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index c32fc74fa5..5bc413036a 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -350,7 +350,7 @@ Player scripts are local scripts that are attached to a player. +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.input ` | by player scripts | | User input | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.ui ` | by player scripts | | Controls user interface | +|:ref:`openmw.ui ` | by player scripts | | Controls :ref:`user interface ` | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |openmw.camera | by player scripts | | Controls camera (not implemented) | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ @@ -750,5 +750,3 @@ You can add special hints to give LDT more information: .. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-code-completion2.png See `LDT Documentation Language `__ for more details. - - diff --git a/docs/source/reference/lua-scripting/user_interface.rst b/docs/source/reference/lua-scripting/user_interface.rst new file mode 100644 index 0000000000..5b733a8fe9 --- /dev/null +++ b/docs/source/reference/lua-scripting/user_interface.rst @@ -0,0 +1,151 @@ +User interface reference +======================== + +.. toctree:: + :hidden: + + widgets/widget + +Layouts +------- + +Every widget is defined by a layout, which is a Lua table with the following fields (all of them are optional): + +1. `type`: One of the available widget types from `openmw.ui.TYPE`. +2. | `props`: A Lua table, containing all the properties values. + | Properties define most of the information about the widget: its position, data it displays, etc. + | See the widget pages (table below) for details on specific properties. + | Properties of the basic Widget are inherited by all the other widgets. +3. | `events`: A Lua table, containing `openmw.async.callback` values, which trigger on various interactions with the widget. + | See the Widget pages for details on specific events. + | Events of the basic Widget are inherited by all the other widgets. +4. `content`: a Content (`openmw.ui.content`), which contains layouts for the children of this widget. +5. | `name`: an arbitrary string, the only limitatiion is it being unique within a `Content`. + | Helpful for navigatilng through the layouts. +6. `layer`: only applies for the root widget. (Windows, HUD, etc) + +.. TODO: Write a more detailed documentation for layers when they are finished + +Elements +-------- + +Element is the root widget of a layout. +It is an independent part of the UI, connected only to a specific layer, but not any other layouts. +Creating or destroying an element also creates/destroys all of its children. + +Content +------- + +A container holding all the widget's children. It has a few important differences from a Lua table: + +1. All the keys are integers, i. e. it is an "array" +2. Holes are not allowed. At any point all keys from `1` to the highest `n` must contain a value. +3. | You can access the values by their `name` field as a `Content` key. + | While there is nothing preventing you from changing the `name` of a table inside a content, it is not supported, and will lead to undefined behaviour. + | If you have to change the name, assign a new table to the index instead. + +.. TODO: Talk about skins/templates here when they are ready + +Widget types +------------ + +.. list-table:: + :widths: 30 70 + + * - :ref:`Widget` + - Base widget type, all the other widget inherit its properties and events. + * - `Text` + - Displays text. + * - EditText + - Accepts text input from the user. + * - Window + - Can be moved and resized by the user. + +Example +------- + +*scripts/requirePassword.lua* + +.. code-block:: Lua + + local core = require('openmw.core') + local async = require('openmw.async') + local ui = require('openmw.ui') + local v2 = require('openmw.util').vector2 + + local layout = { + layers = 'Windows', + type = ui.TYPE.Window, + skin = 'MW_Window', -- TODO: replace all skins here when they are properly implemented + props = { + size = v2(200, 250), + -- put the window in the middle of the screen + relativePosition = v2(0.5, 0.5), + anchor = v2(0.5, 0.5), + }, + content = ui.content { + { + type = ui.TYPE.Text, + skin = 'SandText', + props = { + caption = 'Input password', + relativePosition = v2(0.5, 0), + anchor = v2(0.5, 0), + }, + }, + { + name = 'input', + type = ui.TYPE.TextEdit, + skin = "MW_TextEdit", + props = { + caption = '', + relativePosition = v2(0.5, 0.5), + anchor = v2(0.5, 0.5), + size = v2(125, 50), + }, + events = {} + }, + { + name = 'submit', + type = ui.TYPE.Text, -- TODO: replace with button when implemented + skin = "MW_Button", + props = { + caption = 'Submit', + -- position at the bottom + relativePosition = v2(0.5, 1.0), + anchor = v2(0.5, 1.0), + autoSize = false, + size = v2(75, 50), + }, + events = {}, + }, + }, + } + + local element = nil + + local input = layout.content.input + -- TODO: replace with a better event when TextEdit is finished + input.events.textInput = async:callback(function(text) + input.props.caption = input.props.caption .. text + end) + + local submit = layout.content.submit + submit.events.mouseClick = async:callback(function() + if input.props.caption == 'very secret password' then + if element then + element:destroy() + end + else + print('wrong password', input.props.caption) + core.quit() + end + end) + + element = ui.create(layout) + +*requirePassword.omwscripts* + +:: + + PLAYER: scripts/requirePassword.lua diff --git a/docs/source/reference/lua-scripting/widgets/widget.rst b/docs/source/reference/lua-scripting/widgets/widget.rst new file mode 100644 index 0000000000..51d6d17203 --- /dev/null +++ b/docs/source/reference/lua-scripting/widgets/widget.rst @@ -0,0 +1,77 @@ +Widget +====== + +Properties +---------- + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type (default value) + - description + * - position + - util.vector2 (0, 0) + - | Offsets the position of the widget from its parent's + | top-left corner in pixels. + * - size + - util.vector2 (0, 0) + - Increases the widget's size in pixels. + * - relativePosition + - util.vector2 (0, 0) + - | Offsets the position of the widget from its parent's + | top-left corner as a fraction of the parent's size. + * - relativeSize + - util.vector2 (0, 0) + - Increases the widget's size by a fraction of its parent's size. + * - anchor + - util.vector2 (0, 0) + - | Offsets the widget's position by a fraction of its size. + | Useful for centering or aligning to a corner. + * - visible + - boolean (true) + - Defines if the widget is visible + +Events +------ + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - keyPress + - `KeyboardEvent <../openmw_input.html##(KeyboardEvent)>`_ + - A key was pressed with this widget in focus + * - keyRelease + - `KeyboardEvent <../openmw_input.html##(KeyboardEvent)>`_ + - A key was released with this widget in focus + * - mouseMove + - `MouseEvent <../openmw_ui.html##(MouseEvent)>`_ + - | Mouse cursor moved on this widget + | `MouseEvent.button` is the mouse button being held + | (nil when simply moving, and not dragging) + * - mouseClick + - nil + - Widget was clicked with left mouse button + * - mouseDoubleClick + - nil + - Widget was double clicked with left mouse button + * - mousePress + - `MouseEvent <../openmw_ui.html##(MouseEvent)>`_ + - A mouse button was pressed on this widget + * - mouseRelease + - `MouseEvent <../openmw_ui.html##(MouseEvent)>`_ + - A mouse button was released on this widget + * - focusGain + - nil + - Widget gained focus (either through mouse or keyboard) + * - focusLoss + - nil + - Widget lost focus + * - textInput + - string + - Text input with this widget in focus diff --git a/files/lua_api/openmw/ui.lua b/files/lua_api/openmw/ui.lua index bf6976c76b..593cbf8043 100644 --- a/files/lua_api/openmw/ui.lua +++ b/files/lua_api/openmw/ui.lua @@ -1,15 +1,115 @@ -------------------------------------------------------------------------------- +--- -- `openmw.ui` controls user interface. -- Can be used only by local scripts, that are attached to a player. -- @module ui --- @usage local ui = require('openmw.ui') +-- @usage +-- local ui = require('openmw.ui') +--- +-- @field [parent=#ui] #WIDGET_TYPE WIDGET_TYPE +--- +-- @type WIDGET_TYPE +-- @field [parent=#WIDGET_TYPE] Widget Base widget type +-- @field [parent=#WIDGET_TYPE] Text Display text +-- @field [parent=#WIDGET_TYPE] TextEdit Accepts user text input +-- @field [parent=#WIDGET_TYPE] Window Can be moved and resized by the user -------------------------------------------------------------------------------- +--- -- Shows given message at the bottom of the screen. -- @function [parent=#ui] showMessage -- @param #string msg -return nil +--- +-- Converts a given table of tables into an @{openmw.ui#Content} +-- @function [parent=#ui] content +-- @param #table table +-- @return #Content +--- +-- Creates a UI element from the given layout table +-- @function [parent=#ui] create +-- @param #Layout layout +-- @return #Element + +--- +-- Layout +-- @type Layout +-- @field #string name Optional name of the layout. Allows access by name from Content +-- @field #table props Optional table of widget properties +-- @field #table events Optional table of event callbacks +-- @field #Content content Optional @{openmw.ui#Content} of children layouts + +--- +-- Content. An array-like container, which allows to reference elements by their name +-- @type Content +-- @list <#Layout> +-- @usage +-- local content = ui.content { +-- { name = 'input' }, +-- } +-- -- bad idea! +-- -- content[1].name = 'otherInput' +-- -- do this instead: +-- content.input = { name = 'otherInput' } +-- @usage +-- local content = ui.content { +-- { name = 'display' }, +-- { name = 'submit' }, +-- } +-- -- allowed, but shifts all the items after it "up" the array +-- content.display = nil +-- -- still no holes after this! +-- @usage +-- -- iterate over a Content +-- for i = 1, #content do +-- print('widget',content[i].name,'at',i) +-- end + +--- +-- Puts the layout at given index by shifting all the elements after it +-- @function [parent=#Content] insert +-- @param self +-- @param #number index +-- @param #Layout layout + +--- +-- Adds the layout at the end of the Content +-- (same as calling insert with `last index + 1`) +-- @function [parent=#Content] add +-- @param self +-- @param #Layout layout + +--- +-- Finds the index of the given layout. If it is not in the container, returns nil +-- @function [parent=#Content] indexOf +-- @param self +-- @param #Layout layout +-- @return #number, #nil index + +--- +-- Element. An element of the user interface +-- @type Element + +--- +-- Refreshes the rendered element to match the current layout state +-- @function [parent=#Element] update +-- @param self + +--- +-- Destroys the element +-- @function [parent=#Element] destroy +-- @param self + +--- +-- Access or replace the element's layout +-- @field [parent=#Element] #Layout layout + +--- +-- Mouse event, passed as an argument to relevant UI events +-- @type MouseEvent +-- @field openmw.util#Vector2 position Absolute position of the mouse cursor +-- @field openmw.util#Vector2 offset Position of the mouse cursor relative to the widget +-- @field #number button Mouse button which triggered the event (could be nil) + +return nil