diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index d42f7b0637..98a653949d 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -95,6 +95,13 @@ namespace MWLua MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); auto element = context.mLua->sol().new_usertype("Element"); + element[sol::meta_function::to_string] = [](const LuaUi::Element& element) { + std::stringstream res; + res << "UiElement"; + if (element.mLayer != "") + res << "[" << element.mLayer << "]"; + return res.str(); + }; element["layout"] = sol::property([](LuaUi::Element& element) { return element.mLayout; }, [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { diff --git a/components/lua_ui/adapter.cpp b/components/lua_ui/adapter.cpp index 2fd6365977..44493f6f46 100644 --- a/components/lua_ui/adapter.cpp +++ b/components/lua_ui/adapter.cpp @@ -18,11 +18,16 @@ namespace LuaUi { mContainer = MyGUI::Gui::getInstancePtr()->createWidget( "", MyGUI::IntCoord(), MyGUI::Align::Default, "", ""); - mContainer->initialize(luaState, mContainer); - mContainer->onCoordChange([this](WidgetExtension* ext, MyGUI::IntCoord coord) { setSize(coord.size()); }); + mContainer->initialize(luaState, mContainer, false); + mContainer->widget()->eventChangeCoord += MyGUI::newDelegate(this, &LuaAdapter::containerChangedCoord); mContainer->widget()->attachToWidget(this); } + void LuaAdapter::containerChangedCoord(MyGUI::Widget*) + { + setSize(mContainer->getSize()); + } + void LuaAdapter::attach(const std::shared_ptr& element) { detachElement(); @@ -44,14 +49,20 @@ namespace LuaUi void LuaAdapter::attachElement() { - if (mElement.get()) - mElement->attachToWidget(mContainer); + if (!mElement.get()) + return; + if (!mElement->mRoot) + throw std::logic_error("Attempting to use a destroyed UI Element"); + mContainer->setChildren({ mElement->mRoot }); + mElement->mRoot->updateCoord(); + mContainer->updateCoord(); } void LuaAdapter::detachElement() { - if (mElement.get()) - mElement->detachFromWidget(); + mContainer->setChildren({}); + if (mElement && mElement->mRoot) + mElement->mRoot->widget()->detachFromWidget(); mElement = nullptr; } } diff --git a/components/lua_ui/adapter.hpp b/components/lua_ui/adapter.hpp index d699e4992f..1524a55425 100644 --- a/components/lua_ui/adapter.hpp +++ b/components/lua_ui/adapter.hpp @@ -25,6 +25,8 @@ namespace LuaUi void attachElement(); void detachElement(); + + void containerChangedCoord(MyGUI::Widget*); }; } diff --git a/components/lua_ui/container.hpp b/components/lua_ui/container.hpp index 79d3cd8fa8..16f19d3c12 100644 --- a/components/lua_ui/container.hpp +++ b/components/lua_ui/container.hpp @@ -9,6 +9,7 @@ namespace LuaUi { MYGUI_RTTI_DERIVED(LuaContainer) + public: MyGUI::IntSize calculateSize() override; void updateCoord() override; diff --git a/components/lua_ui/content.cpp b/components/lua_ui/content.cpp index 2e1d4ca0c4..dd169a9291 100644 --- a/components/lua_ui/content.cpp +++ b/components/lua_ui/content.cpp @@ -1,4 +1,5 @@ #include "content.hpp" +#include "element.hpp" namespace LuaUi { @@ -14,6 +15,8 @@ namespace LuaUi bool isValidContent(const sol::object& object) { + if (object.is()) + return true; if (object.get_type() != sol::type::table) return false; sol::table table = object; diff --git a/components/lua_ui/content.hpp b/components/lua_ui/content.hpp index c8bb82ecf3..1a0379b817 100644 --- a/components/lua_ui/content.hpp +++ b/components/lua_ui/content.hpp @@ -22,7 +22,7 @@ namespace LuaUi : mTable(std::move(table)) { if (!isValidContent(mTable)) - throw std::domain_error("Expected a Content table"); + throw std::domain_error("Invalid UI Content"); } size_t size() const { return mTable.size(); } @@ -43,17 +43,17 @@ namespace LuaUi } void insert(size_t index, const sol::table& table) { callMethod("insert", toLua(index), table); } - sol::table at(size_t index) const + sol::object at(size_t index) const { if (index < size()) - return mTable.get(toLua(index)); + return mTable.get(toLua(index)); else throw std::range_error("Invalid Content index"); } - sol::table at(std::string_view name) const + sol::object at(std::string_view name) const { if (indexOf(name).has_value()) - return mTable.get(name); + return mTable.get(name); else throw std::range_error("Invalid Content key"); } diff --git a/components/lua_ui/content.lua b/components/lua_ui/content.lua index fbd39d5f68..99fdb86b70 100644 --- a/components/lua_ui/content.lua +++ b/components/lua_ui/content.lua @@ -1,12 +1,17 @@ local M = {} M.__Content = true + +function validateContentChild(v) + if not (type(v) == 'table' or v.__type and v.__type.name == 'LuaUi::Element') then + error('Content can only contain tables and Elements') + end +end + 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 + validateContentChild(v) result[i] = v if type(v.name) == 'string' then result.__nameIndex[v.name] = i @@ -38,9 +43,7 @@ end local methods = { insert = function(self, index, value) validateIndex(self, index) - if type(value) ~= 'table' then - error('Content can only contain tables') - end + validateContentChild(value) for i = #self, index, -1 do rawset(self, i + 1, rawget(self, i)) local name = rawget(self, i + 1) @@ -56,7 +59,7 @@ local methods = { indexOf = function(self, value) if type(value) == 'string' then return self.__nameIndex[value] - elseif type(value) == 'table' then + else for i = 1, #self do if rawget(self, i) == value then return i @@ -113,10 +116,9 @@ 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') + validateContentChild(value) + assign(self, index, value) end end M.__tostring = function(self) diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp index 4fe9349b9e..71f2ba9c96 100644 --- a/components/lua_ui/element.cpp +++ b/components/lua_ui/element.cpp @@ -43,12 +43,70 @@ namespace LuaUi return type; } - void destroyWidget(LuaUi::WidgetExtension* ext) + void destroyWidget(WidgetExtension* ext) { ext->deinitialize(); MyGUI::Gui::getInstancePtr()->destroyWidget(ext->widget()); } + void destroyChild(WidgetExtension* ext) + { + if (!ext->isRoot()) + destroyWidget(ext); + else + ext->widget()->detachFromWidget(); + } + + void detachElements(WidgetExtension* ext) + { + for (auto* child : ext->children()) + { + if (child->isRoot()) + child->widget()->detachFromWidget(); + else + detachElements(child); + } + for (auto* child : ext->templateChildren()) + { + if (child->isRoot()) + child->widget()->detachFromWidget(); + else + detachElements(child); + } + } + + void destroyRoot(WidgetExtension* ext) + { + detachElements(ext); + destroyWidget(ext); + } + + void updateRootCoord(WidgetExtension* ext) + { + WidgetExtension* root = ext; + while (root->getParent()) + root = root->getParent(); + root->updateCoord(); + } + + WidgetExtension* pluckElementRoot(const sol::object& child) + { + std::shared_ptr element = child.as>(); + WidgetExtension* root = element->mRoot; + if (!root) + throw std::logic_error("Using a destroyed element as a layout child"); + WidgetExtension* parent = root->getParent(); + if (parent) + { + auto children = parent->children(); + std::remove(children.begin(), children.end(), root); + parent->setChildren(children); + root->widget()->detachFromWidget(); + } + root->updateCoord(); + return root; + } + WidgetExtension* createWidget(const sol::table& layout, uint64_t depth); void updateWidget(WidgetExtension* ext, const sol::table& layout, uint64_t depth); @@ -60,7 +118,7 @@ namespace LuaUi if (contentObj == sol::nil) { for (WidgetExtension* w : children) - destroyWidget(w); + destroyChild(w); return result; } ContentView content(LuaUtil::cast(contentObj)); @@ -69,22 +127,39 @@ namespace LuaUi for (size_t i = 0; i < minSize; i++) { WidgetExtension* ext = children[i]; - sol::table newLayout = content.at(i); - if (ext->widget()->getTypeName() == widgetType(newLayout)) + sol::object child = content.at(i); + if (child.is()) { - updateWidget(ext, newLayout, depth); + WidgetExtension* root = pluckElementRoot(child); + if (ext != root) + destroyChild(ext); + result[i] = root; } else { - destroyWidget(ext); - ext = createWidget(newLayout, depth); + sol::table newLayout = child.as(); + if (ext->widget()->getTypeName() == widgetType(newLayout)) + { + updateWidget(ext, newLayout, depth); + } + else + { + destroyChild(ext); + ext = createWidget(newLayout, depth); + } + result[i] = ext; } - result[i] = ext; } for (size_t i = minSize; i < children.size(); i++) - destroyWidget(children[i]); + destroyChild(children[i]); for (size_t i = minSize; i < content.size(); i++) - result[i] = createWidget(content.at(i), depth); + { + sol::object child = content.at(i); + if (child.is()) + result[i] = pluckElementRoot(child); + else + result[i] = createWidget(child.as(), depth); + } return result; } @@ -97,7 +172,7 @@ namespace LuaUi ext->setTemplateChildren(updateContent(ext->templateChildren(), content, depth)); } - void setEventCallbacks(LuaUi::WidgetExtension* ext, const sol::object& eventsObj) + void setEventCallbacks(WidgetExtension* ext, const sol::object& eventsObj) { ext->clearCallbacks(); if (eventsObj == sol::nil) @@ -130,7 +205,7 @@ namespace LuaUi WidgetExtension* ext = dynamic_cast(widget); if (!ext) throw std::runtime_error("Invalid widget!"); - ext->initialize(layout.lua_state(), widget); + ext->initialize(layout.lua_state(), widget, depth == 0); updateWidget(ext, layout, depth); return ext; @@ -169,7 +244,6 @@ namespace LuaUi Element::Element(sol::table layout) : mRoot(nullptr) - , mAttachedTo(nullptr) , mLayout(std::move(layout)) , mLayer() , mUpdate(false) @@ -191,7 +265,7 @@ namespace LuaUi { mRoot = createWidget(layout(), 0); mLayer = setLayer(mRoot, layout()); - updateAttachment(); + updateRootCoord(mRoot); } } @@ -201,15 +275,21 @@ namespace LuaUi { if (mRoot->widget()->getTypeName() != widgetType(layout())) { - destroyWidget(mRoot); + destroyRoot(mRoot); + WidgetExtension* parent = mRoot->getParent(); + auto children = parent->children(); + auto it = std::find(children.begin(), children.end(), mRoot); mRoot = createWidget(layout(), 0); + *it = mRoot; + parent->setChildren(children); + mRoot->updateCoord(); } else { updateWidget(mRoot, layout(), 0); } mLayer = setLayer(mRoot, layout()); - updateAttachment(); + updateRootCoord(mRoot); } mUpdate = false; } @@ -218,40 +298,10 @@ namespace LuaUi { if (mRoot) { - destroyWidget(mRoot); + destroyRoot(mRoot); mRoot = nullptr; mLayout = sol::make_object(mLayout.lua_state(), sol::nil); } sAllElements.erase(this); } - - void Element::attachToWidget(WidgetExtension* w) - { - if (mAttachedTo) - throw std::logic_error("A UI element can't be attached to two widgets at once"); - mAttachedTo = w; - updateAttachment(); - } - - void Element::detachFromWidget() - { - if (mRoot) - mRoot->widget()->detachFromWidget(); - if (mAttachedTo) - mAttachedTo->setChildren({}); - mAttachedTo = nullptr; - } - - void Element::updateAttachment() - { - if (!mRoot) - return; - if (mAttachedTo) - { - if (!mLayer.empty()) - Log(Debug::Warning) << "Ignoring element's layer " << mLayer << " because it's attached to a widget"; - mAttachedTo->setChildren({ mRoot }); - mAttachedTo->updateCoord(); - } - } } diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp index b57af92fee..5aadb1beab 100644 --- a/components/lua_ui/element.hpp +++ b/components/lua_ui/element.hpp @@ -17,7 +17,6 @@ namespace LuaUi } WidgetExtension* mRoot; - WidgetExtension* mAttachedTo; sol::object mLayout; std::string mLayer; bool mUpdate; @@ -31,14 +30,10 @@ namespace LuaUi friend void clearUserInterface(); - void attachToWidget(WidgetExtension* w); - void detachFromWidget(); - private: Element(sol::table layout); sol::table layout() { return LuaUtil::cast(mLayout); } static std::map> sAllElements; - void updateAttachment(); }; } diff --git a/components/lua_ui/widget.cpp b/components/lua_ui/widget.cpp index e3188f6136..9550c9de73 100644 --- a/components/lua_ui/widget.cpp +++ b/components/lua_ui/widget.cpp @@ -9,6 +9,7 @@ namespace LuaUi : mForcePosition(false) , mForceSize(false) , mPropagateEvents(true) + , mVisible(true) , mLua(nullptr) , mWidget(nullptr) , mSlot(this) @@ -18,13 +19,15 @@ namespace LuaUi , mExternal(sol::nil) , mParent(nullptr) , mTemplateChild(false) + , mElementRoot(false) { } - void WidgetExtension::initialize(lua_State* lua, MyGUI::Widget* self) + void WidgetExtension::initialize(lua_State* lua, MyGUI::Widget* self, bool isRoot) { mLua = lua; mWidget = self; + mElementRoot = isRoot; initialize(); updateTemplate(); } @@ -40,8 +43,6 @@ namespace LuaUi clearCallbacks(); clearEvents(mWidget); - mOnCoordChange.reset(); - for (WidgetExtension* w : mChildren) w->deinitialize(); for (WidgetExtension* w : mTemplateChildren) @@ -92,10 +93,9 @@ namespace LuaUi { // workaround for MyGUI bug // parent visibility doesn't affect added children - MyGUI::Widget* widget = this->widget(); - MyGUI::Widget* parent = widget->getParent(); - bool inheritedVisible = widget->getVisible() && (parent == nullptr || parent->getInheritedVisible()); - widget->setVisible(inheritedVisible); + MyGUI::Widget* parent = widget()->getParent(); + bool inheritedVisible = mVisible && (parent == nullptr || parent->getInheritedVisible()); + widget()->setVisible(inheritedVisible); } void WidgetExtension::attach(WidgetExtension* ext) @@ -262,8 +262,6 @@ namespace LuaUi if (oldCoord != newCoord) mWidget->setCoord(newCoord); updateChildrenCoord(); - if (oldCoord != newCoord && mOnCoordChange.has_value()) - mOnCoordChange.value()(this, newCoord); } void WidgetExtension::setProperties(const sol::object& props) @@ -280,7 +278,8 @@ namespace LuaUi mRelativeCoord = propertyValue("relativePosition", MyGUI::FloatPoint()); mRelativeCoord = propertyValue("relativeSize", MyGUI::FloatSize()); mAnchor = propertyValue("anchor", MyGUI::FloatSize()); - mWidget->setVisible(propertyValue("visible", true)); + mVisible = propertyValue("visible", true); + mWidget->setVisible(mVisible); mWidget->setPointer(propertyValue("pointer", std::string("arrow"))); mWidget->setAlpha(propertyValue("alpha", 1.f)); mWidget->setInheritsAlpha(propertyValue("inheritAlpha", true)); diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp index 81698b0479..c72b64ae3b 100644 --- a/components/lua_ui/widget.hpp +++ b/components/lua_ui/widget.hpp @@ -26,13 +26,16 @@ namespace LuaUi virtual ~WidgetExtension() = default; // must be called after creating the underlying MyGUI::Widget - void initialize(lua_State* lua, MyGUI::Widget* self); + void initialize(lua_State* lua, MyGUI::Widget* self, bool isRoot); // must be called after before destroying the underlying MyGUI::Widget virtual void deinitialize(); MyGUI::Widget* widget() const { return mWidget; } WidgetExtension* slot() const { return mSlot; } + bool isRoot() const { return mElementRoot; } + WidgetExtension* getParent() const { return mParent; } + void reset(); const std::vector& children() { return mChildren; } @@ -66,11 +69,6 @@ namespace LuaUi return parseExternal(mExternal, name, defaultValue); } - void onCoordChange(const std::optional>& callback) - { - mOnCoordChange = callback; - } - virtual MyGUI::IntSize calculateSize(); virtual MyGUI::IntPoint calculatePosition(const MyGUI::IntSize& size); MyGUI::IntCoord calculateCoord(); @@ -137,6 +135,7 @@ namespace LuaUi MyGUI::FloatSize mAnchor; bool mPropagateEvents; + bool mVisible; // used to implement updateVisible private: // use lua_State* instead of sol::state_view because MyGUI requires a default constructor @@ -152,6 +151,7 @@ namespace LuaUi sol::object mExternal; WidgetExtension* mParent; bool mTemplateChild; + bool mElementRoot; void attach(WidgetExtension* ext); void attachTemplate(WidgetExtension* ext); @@ -172,8 +172,6 @@ namespace LuaUi void focusGain(MyGUI::Widget*, MyGUI::Widget*); void focusLoss(MyGUI::Widget*, MyGUI::Widget*); - std::optional> mOnCoordChange; - void updateVisible(); }; diff --git a/files/lua_api/openmw/ui.lua b/files/lua_api/openmw/ui.lua index 2fefe4fd84..451f919077 100644 --- a/files/lua_api/openmw/ui.lua +++ b/files/lua_api/openmw/ui.lua @@ -164,9 +164,9 @@ --- -- Content. An array-like container, which allows to reference elements by their name. --- Implements [iterables#List](iterables.html#List) of #Layout and [iterables#Map](iterables.html#Map) of #string to #Layout. +-- Implements [iterables#List](iterables.html#List) of #Layout or #Element and [iterables#Map](iterables.html#Map) of #string to #Layout or #Element. -- @type Content --- @list <#Layout> +-- @list <#any> -- @usage -- local content = ui.content { -- { name = 'input' }, @@ -200,27 +200,27 @@ -- @function [parent=#Content] __index -- @param self -- @param #string name --- @return #Layout +-- @return #any --- -- 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 +-- @param #any layoutOrElement --- -- 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 +-- @param #any layoutOrElement --- -- 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 +-- @param #any layoutOrElement -- @return #number, #nil index --- @@ -228,10 +228,35 @@ -- @type Element --- --- Refreshes the rendered element to match the current layout state +-- Refreshes the rendered element to match the current layout state. +-- Refreshes positions and sizes, but not the layout of the child Elements. -- @function [parent=#Element] update -- @param self +-- @usage +-- local child = ui.create { +-- type = ui.TYPE.Text, +-- props = { +-- text = 'child 1', +-- }, +-- } +-- local parent = ui.create { +-- content = ui.content { +-- child, +-- { +-- type = ui.TYPE.Text, +-- props = { +-- text = 'parent 1', +-- }, +-- } +-- } +-- } +-- -- ... +-- child.layout.props.text = 'child 2' +-- parent.layout.content[2].props.text = 'parent 2' +-- parent:update() -- will show 'parent 2', but 'child 1' + + --- -- Destroys the element -- @function [parent=#Element] destroy