Merge pull request #520 from MihailRis/custom-ui-elements

Custom UI elements
This commit is contained in:
MihailRis 2025-04-27 14:27:47 +03:00 committed by GitHub
commit ddd09a23d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 150 additions and 33 deletions

View File

@ -164,9 +164,10 @@ Properties:
Properties: Properties:
| Name | Type | Read | Write | Description | | Name | Type | Read | Write | Description |
| ----- | ------ | ---- | ----- | ------------ | | ------ | ------ | ---- | ----- | ---------------- |
| src | string | yes | yes | texture name | | src | string | yes | yes | texture name |
| region | vec4 | yes | yes | image sub-region |
## Canvas ## Canvas

View File

@ -40,6 +40,8 @@ Examples:
- `size-func` - element size provider (two numbers), called when the size of the container in which the element is located changes, or when an element is added to the container. Can be called before on_hud_open is called. - `size-func` - element size provider (two numbers), called when the size of the container in which the element is located changes, or when an element is added to the container. Can be called before on_hud_open is called.
- `onclick` - lua function called when an element is clicked. - `onclick` - lua function called when an element is clicked.
- `ondoubleclick` - lua function called when you double click on an element. - `ondoubleclick` - lua function called when you double click on an element.
- `onfocus` - lua function called when focusing on an element.
- `ondefocus` - lua function called when the element loses focus.
- `tooltip` - tooltip text - `tooltip` - tooltip text
- `tooltip-delay` - tooltip show-up delay - `tooltip-delay` - tooltip show-up delay
- `gravity` - automatic positioning of the element in the container. (Does not work in automatic containers like panel). Values: *top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right*. - `gravity` - automatic positioning of the element in the container. (Does not work in automatic containers like panel). Values: *top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right*.
@ -112,6 +114,7 @@ Inner text is a button text.
- `src` - name of an image stored in textures folder. Extension is not specified. Type: string. - `src` - name of an image stored in textures folder. Extension is not specified. Type: string.
Example: *gui/error* Example: *gui/error*
- `region` - image region x1, y1, x2, y2 from 0.0, 0.0 (upper left corner), 1.0, 1.0 (lower right corner)
## *canvas* ## *canvas*

View File

@ -164,9 +164,10 @@ document["worlds-panel"]:clear()
Свойства: Свойства:
| Название | Тип | Чтение | Запись | Описание | | Название | Тип | Чтение | Запись | Описание |
| -------- | ------ | ------ | ------ | --------------------- | | -------- | ------ | ------ | ------ | ---------------------- |
| src | string | да | да | отображаемая текстура | | src | string | да | да | отображаемая текстура |
| region | vec4 | да | да | под-регион изображения |
## Холст (canvas) ## Холст (canvas)

View File

@ -44,6 +44,8 @@
- `size-func` - поставщик размера элемента (два числа), вызываемый при изменении размера контейнера, в котором находится элемент, либо при добавлении элемента в контейнер. Может быть вызван до вызова on_hud_open. - `size-func` - поставщик размера элемента (два числа), вызываемый при изменении размера контейнера, в котором находится элемент, либо при добавлении элемента в контейнер. Может быть вызван до вызова on_hud_open.
- `onclick` - lua функция вызываемая при нажатии на элемент. - `onclick` - lua функция вызываемая при нажатии на элемент.
- `ondoubleclick` - lua функция вызываемая при двойном нажатии на элемент. - `ondoubleclick` - lua функция вызываемая при двойном нажатии на элемент.
- `onfocus` - lua функция вызываемая при фокусировке на элемент.
- `ondefocus` - lua функция вызываемая при потере фокуса элеметом.
- `tooltip` - текст всплывающей подсказки - `tooltip` - текст всплывающей подсказки
- `tooltip-delay` - задержка появления всплывающей подсказки - `tooltip-delay` - задержка появления всплывающей подсказки
- `gravity` - автоматическое позиционирование элемента в контейнере. (Не работает в автоматических контейнерах, как panel). Значения: *top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right*. - `gravity` - автоматическое позиционирование элемента в контейнере. (Не работает в автоматических контейнерах, как panel). Значения: *top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right*.
@ -113,6 +115,7 @@
## Изображение - *image* ## Изображение - *image*
- `src` - имя изображения в папке textures без указания расширения. Тип: строка. Например `gui/error` - `src` - имя изображения в папке textures без указания расширения. Тип: строка. Например `gui/error`
- `region` - под-регион изображения x1, y1, x2, y2 от 0.0, 0.0 (левый верхний угол), 1.0, 1.0 (правый нижний угол)
## Холст - *canvas* ## Холст - *canvas*

View File

@ -226,15 +226,20 @@ end
function gui.template(name, params) function gui.template(name, params)
local text = file.read(file.find("layouts/templates/"..name..".xml")) local text = file.read(file.find("layouts/templates/"..name..".xml"))
for k,v in pairs(params) do text = text:gsub("%%{([^}]+)}", function(n)
local arg = tostring(v):gsub("'", "\\'"):gsub('"', '\\"') local s = params[n]
text = text:gsub("(%%{"..k.."})", arg) if type(s) ~= "string" then
end return tostring(s)
text = text:gsub("if%s*=%s*'%%{%w+}'", "if=''") end
text = text:gsub("if%s*=%s*\"%%{%w+}\"", "if=\"\"") if #s == 0 then
return
end
local e = string.escape(s)
return e:sub(2, #e-1)
end)
text = text:gsub('if%s*=%s*[\'"]%%{%w+}[\'"]', "if=\"\"")
-- remove unsolved properties: attr='%{var}' -- remove unsolved properties: attr='%{var}'
text = text:gsub("%s*%S+='%%{[^}]+}'%s*", " ") text = text:gsub('%s*%S+=[\'"]%%{[^}]+}[\'"]%s*', " ")
text = text:gsub('%s*%S+="%%{[^}]+}"%s*', " ")
return text return text
end end

View File

@ -53,11 +53,11 @@ namespace xml {
/// @brief Get element tag /// @brief Get element tag
const std::string& getTag() const; const std::string& getTag() const;
inline bool isText() const { bool isText() const {
return getTag() == "#"; return getTag() == "#";
} }
inline const std::string& text() const { const std::string& getInnerText() const {
return attr("#").getText(); return attr("#").getText();
} }

View File

@ -67,7 +67,7 @@ std::unique_ptr<UiDocument> UiDocument::read(
? scripting::create_doc_environment(scripting::get_root_environment(), name) ? scripting::create_doc_environment(scripting::get_root_environment(), name)
: scripting::create_doc_environment(penv, name); : scripting::create_doc_environment(penv, name);
gui::UiXmlReader reader(gui, env); gui::UiXmlReader reader(gui, scriptenv(env));
auto view = reader.readXML(file.string(), *xmldoc->getRoot()); auto view = reader.readXML(file.string(), *xmldoc->getRoot());
view->setId("root"); view->setId("root");
uidocscript script {}; uidocscript script {};

View File

@ -71,6 +71,7 @@ void Container::mouseRelease(int x, int y) {
} }
void Container::act(float delta) { void Container::act(float delta) {
UINode::act(delta);
for (const auto& node : nodes) { for (const auto& node : nodes) {
if (node->isVisible()) { if (node->isVisible()) {
node->act(delta); node->act(delta);
@ -162,7 +163,13 @@ void Container::add(const std::shared_ptr<UINode>& node) {
nodes.push_back(node); nodes.push_back(node);
node->setParent(this); node->setParent(this);
node->reposition(); node->reposition();
refresh(); mustRefresh = true;
auto parent = getParent();
while (parent) {
parent->setMustRefresh();
parent = parent->getParent();
}
} }
void Container::add(const std::shared_ptr<UINode>& node, glm::vec2 pos) { void Container::add(const std::shared_ptr<UINode>& node, glm::vec2 pos) {
@ -202,7 +209,6 @@ void Container::listenInterval(float interval, ontimeout callback, int repeat) {
void Container::setSize(glm::vec2 size) { void Container::setSize(glm::vec2 size) {
if (size == getSize()) { if (size == getSize()) {
refresh();
return; return;
} }
UINode::setSize(size); UINode::setSize(size);

View File

@ -55,7 +55,7 @@ void Image::draw(const DrawContext& pctx, const Assets& assets) {
0, 0,
0, 0,
0, 0,
UVRegion(), region,
false, false,
true, true,
calcColor() calcColor()
@ -76,3 +76,11 @@ const std::string& Image::getTexture() const {
void Image::setTexture(const std::string& name) { void Image::setTexture(const std::string& name) {
texture = name; texture = name;
} }
void Image::setRegion(const UVRegion& region) {
this->region = region;
}
const UVRegion& Image::getRegion() const {
return region;
}

View File

@ -1,11 +1,13 @@
#pragma once #pragma once
#include "UINode.hpp" #include "UINode.hpp"
#include "maths/UVRegion.hpp"
namespace gui { namespace gui {
class Image : public UINode { class Image : public UINode {
protected: protected:
std::string texture; std::string texture;
UVRegion region {};
bool autoresize = false; bool autoresize = false;
public: public:
Image(GUI& gui, std::string texture, glm::vec2 size=glm::vec2(32,32)); Image(GUI& gui, std::string texture, glm::vec2 size=glm::vec2(32,32));
@ -16,5 +18,7 @@ namespace gui {
virtual bool isAutoResize() const; virtual bool isAutoResize() const;
virtual const std::string& getTexture() const; virtual const std::string& getTexture() const;
virtual void setTexture(const std::string& name); virtual void setTexture(const std::string& name);
void setRegion(const UVRegion& region);
const UVRegion& getRegion() const;
}; };
} }

View File

@ -74,6 +74,16 @@ UINode* UINode::listenDoubleClick(const onaction& action) {
return this; return this;
} }
UINode* UINode::listenFocus(const onaction& action) {
focusCallbacks.listen(action);
return this;
}
UINode* UINode::listenDefocus(const onaction& action) {
defocusCallbacks.listen(action);
return this;
}
void UINode::click(int, int) { void UINode::click(int, int) {
pressed = true; pressed = true;
} }
@ -96,8 +106,14 @@ bool UINode::isPressed() const {
return pressed; return pressed;
} }
void UINode::onFocus() {
focused = true;
focusCallbacks.notify(gui);
}
void UINode::defocus() { void UINode::defocus() {
focused = false; focused = false;
defocusCallbacks.notify(gui);
} }
bool UINode::isFocused() const { bool UINode::isFocused() const {

View File

@ -66,6 +66,7 @@ namespace gui {
class UINode : public std::enable_shared_from_this<UINode> { class UINode : public std::enable_shared_from_this<UINode> {
protected: protected:
GUI& gui; GUI& gui;
bool mustRefresh = true;
private: private:
/// @brief element identifier used for direct access in UiDocument /// @brief element identifier used for direct access in UiDocument
std::string id = ""; std::string id = "";
@ -114,6 +115,10 @@ namespace gui {
ActionsSet actions; ActionsSet actions;
/// @brief 'ondoubleclick' callbacks /// @brief 'ondoubleclick' callbacks
ActionsSet doubleClickCallbacks; ActionsSet doubleClickCallbacks;
/// @brief 'onfocus' callbacks
ActionsSet focusCallbacks;
/// @brief 'ondefocus' callbacks
ActionsSet defocusCallbacks;
/// @brief element tooltip text /// @brief element tooltip text
std::wstring tooltip; std::wstring tooltip;
/// @brief element tooltip delay /// @brief element tooltip delay
@ -127,7 +132,12 @@ namespace gui {
/// @brief Called every frame for all visible elements /// @brief Called every frame for all visible elements
/// @param delta delta timУ /// @param delta delta timУ
virtual void act(float delta) {}; virtual void act(float delta) {
if (mustRefresh) {
mustRefresh = false;
refresh();
}
};
virtual void draw(const DrawContext& pctx, const Assets& assets) = 0; virtual void draw(const DrawContext& pctx, const Assets& assets) = 0;
virtual void setVisible(bool flag); virtual void setVisible(bool flag);
@ -169,10 +179,12 @@ namespace gui {
/// @brief Get element z-index /// @brief Get element z-index
int getZIndex() const; int getZIndex() const;
virtual UINode* listenAction(const onaction &action); virtual UINode* listenAction(const onaction& action);
virtual UINode* listenDoubleClick(const onaction &action); virtual UINode* listenDoubleClick(const onaction& action);
virtual UINode* listenFocus(const onaction& action);
virtual UINode* listenDefocus(const onaction& action);
virtual void onFocus() {focused = true;} virtual void onFocus();
virtual void doubleClick(int x, int y); virtual void doubleClick(int x, int y);
virtual void click(int x, int y); virtual void click(int x, int y);
virtual void clicked(Mousecode button) {} virtual void clicked(Mousecode button) {}
@ -269,5 +281,9 @@ namespace gui {
const std::shared_ptr<UINode>& node, const std::shared_ptr<UINode>& node,
const std::string& id const std::string& id
); );
void setMustRefresh() {
mustRefresh = true;
}
}; };
} }

View File

@ -21,7 +21,7 @@ std::shared_ptr<gui::UINode> guiutil::create(
if (env == nullptr) { if (env == nullptr) {
env = scripting::get_root_environment(); env = scripting::get_root_environment();
} }
UiXmlReader reader(gui, env); UiXmlReader reader(gui, std::move(env));
return reader.readXML("[string]", source); return reader.readXML("[string]", source);
} }

View File

@ -63,7 +63,7 @@ static runnable create_runnable(
const std::string& name const std::string& name
) { ) {
if (element.has(name)) { if (element.has(name)) {
std::string text = element.attr(name).getText(); const std::string& text = element.attr(name).getText();
if (!text.empty()) { if (!text.empty()) {
return scripting::create_runnable( return scripting::create_runnable(
reader.getEnvironment(), text, reader.getFilename() reader.getEnvironment(), text, reader.getFilename()
@ -180,6 +180,14 @@ static void read_uinode(
node.listenAction(onclick); node.listenAction(onclick);
} }
if (auto onfocus = create_action(reader, element, "onfocus")) {
node.listenFocus(onfocus);
}
if (auto ondefocus = create_action(reader, element, "ondefocus")) {
node.listenDefocus(ondefocus);
}
if (auto ondoubleclick = create_action(reader, element, "ondoubleclick")) { if (auto ondoubleclick = create_action(reader, element, "ondoubleclick")) {
node.listenDoubleClick(ondoubleclick); node.listenDoubleClick(ondoubleclick);
} }
@ -279,7 +287,7 @@ static std::wstring parse_inner_text(
) { ) {
std::wstring text = L""; std::wstring text = L"";
if (element.size() == 1) { if (element.size() == 1) {
std::string source = element.sub(0).attr("#").getText(); std::string source = element.sub(0).getInnerText();
util::trim(source); util::trim(source);
text = util::str2wstr_utf8(source); text = util::str2wstr_utf8(source);
if (text[0] == '@') { if (text[0] == '@') {
@ -379,7 +387,7 @@ static std::shared_ptr<UINode> read_button(
std::shared_ptr<Button> button; std::shared_ptr<Button> button;
auto& elements = element.getElements(); auto& elements = element.getElements();
if (!elements.empty() && elements[0]->getTag() != "#") { if (!elements.empty() && !elements[0]->isText()) {
auto inner = reader.readUINode(*elements.at(0)); auto inner = reader.readUINode(*elements.at(0));
if (inner != nullptr) { if (inner != nullptr) {
button = std::make_shared<Button>(gui, inner, padding); button = std::make_shared<Button>(gui, inner, padding);
@ -536,6 +544,11 @@ static std::shared_ptr<UINode> read_image(
std::string src = element.attr("src", "").getText(); std::string src = element.attr("src", "").getText();
auto image = std::make_shared<Image>(reader.getGUI(), src); auto image = std::make_shared<Image>(reader.getGUI(), src);
read_uinode(reader, element, *image); read_uinode(reader, element, *image);
if (element.has("region")) {
auto vec = element.attr("region").asVec4();
image->setRegion(UVRegion(vec.x, vec.y, vec.z, vec.w));
}
return image; return image;
} }
@ -757,7 +770,7 @@ static std::shared_ptr<UINode> read_iframe(
return iframe; return iframe;
} }
UiXmlReader::UiXmlReader(gui::GUI& gui, const scriptenv& env) : gui(gui), env(env) { UiXmlReader::UiXmlReader(gui::GUI& gui, scriptenv&& env) : gui(gui), env(std::move(env)) {
contextStack.emplace(""); contextStack.emplace("");
add("image", read_image); add("image", read_image);
add("canvas", read_canvas); add("canvas", read_canvas);

View File

@ -20,9 +20,9 @@ namespace gui {
std::unordered_set<std::string> ignored; std::unordered_set<std::string> ignored;
std::stack<std::string> contextStack; std::stack<std::string> contextStack;
std::string filename; std::string filename;
const scriptenv& env; scriptenv env;
public: public:
UiXmlReader(gui::GUI& gui, const scriptenv& env); UiXmlReader(gui::GUI& gui, scriptenv&& env);
void add(const std::string& tag, uinode_reader reader); void add(const std::string& tag, uinode_reader reader);
bool hasReader(const std::string& tag) const; bool hasReader(const std::string& tag) const;

View File

@ -83,11 +83,19 @@ static int l_container_add(lua::State* L) {
} }
auto xmlsrc = lua::require_string(L, 2); auto xmlsrc = lua::require_string(L, 2);
try { try {
auto env = docnode.document->getEnvironment();
if (lua::istable(L, 3)) {
env = create_environment(std::move(env));
lua::pushenv(L, *env);
lua::pushvalue(L, 3);
lua::setfield(L, "DATA");
lua::pop(L);
}
auto subnode = guiutil::create( auto subnode = guiutil::create(
engine->getGUI(), xmlsrc, docnode.document->getEnvironment() engine->getGUI(), xmlsrc, std::move(env)
); );
node->add(subnode);
UINode::getIndices(subnode, docnode.document->getMapWriteable()); UINode::getIndices(subnode, docnode.document->getMapWriteable());
node->add(std::move(subnode));
} catch (const std::exception& err) { } catch (const std::exception& err) {
throw std::runtime_error(err.what()); throw std::runtime_error(err.what());
} }
@ -337,6 +345,14 @@ static int p_get_src(UINode* node, lua::State* L) {
return 0; return 0;
} }
static int p_get_region(UINode* node, lua::State* L) {
if (auto image = dynamic_cast<Image*>(node)) {
const auto& region = image->getRegion();
return lua::pushvec4(L, {region.u1, region.v1, region.u2, region.v2});
}
return 0;
}
static int p_get_data(UINode* node, lua::State* L) { static int p_get_data(UINode* node, lua::State* L) {
if (auto canvas = dynamic_cast<Canvas*>(node)) { if (auto canvas = dynamic_cast<Canvas*>(node)) {
return lua::newuserdata<lua::LuaCanvas>(L, canvas->texture(), canvas->data()); return lua::newuserdata<lua::LuaCanvas>(L, canvas->texture(), canvas->data());
@ -548,6 +564,7 @@ static int l_gui_getattr(lua::State* L) {
{"cursor", p_get_cursor}, {"cursor", p_get_cursor},
{"data", p_get_data}, {"data", p_get_data},
{"parent", p_get_parent}, {"parent", p_get_parent},
{"region", p_get_region},
}; };
auto func = getters.find(attr); auto func = getters.find(attr);
if (func != getters.end()) { if (func != getters.end()) {
@ -651,6 +668,12 @@ static void p_set_src(UINode* node, lua::State* L, int idx) {
iframe->setSrc(lua::require_string(L, idx)); iframe->setSrc(lua::require_string(L, idx));
} }
} }
static void p_set_region(UINode* node, lua::State* L, int idx) {
if (auto image = dynamic_cast<Image*>(node)) {
auto vec = lua::tovec4(L, idx);
image->setRegion(UVRegion(vec.x, vec.y, vec.z, vec.w));
}
}
static void p_set_value(UINode* node, lua::State* L, int idx) { static void p_set_value(UINode* node, lua::State* L, int idx) {
if (auto bar = dynamic_cast<TrackBar*>(node)) { if (auto bar = dynamic_cast<TrackBar*>(node)) {
bar->setValue(lua::tonumber(L, idx)); bar->setValue(lua::tonumber(L, idx));
@ -777,6 +800,7 @@ static int l_gui_setattr(lua::State* L) {
{"inventory", p_set_inventory}, {"inventory", p_set_inventory},
{"cursor", p_set_cursor}, {"cursor", p_set_cursor},
{"focused", p_set_focused}, {"focused", p_set_focused},
{"region", p_set_region},
}; };
auto func = setters.find(attr); auto func = setters.find(attr);
if (func != setters.end()) { if (func != setters.end()) {

View File

@ -154,6 +154,22 @@ std::unique_ptr<Process> scripting::start_coroutine(
}); });
} }
[[nodiscard]] scriptenv scripting::create_environment(
const scriptenv& parent
) {
auto L = lua::get_main_state();
int id = lua::create_environment(L, (parent ? *parent : 0));
lua::pushenv(L, id);
lua::pushvalue(L, -1);
lua::setfield(L, "CUR_ENV");
lua::pop(L);
return std::shared_ptr<int>(new int(id), [=](int* id) { //-V508
lua::remove_environment(L, *id);
delete id;
});
}
[[nodiscard]] scriptenv scripting::create_doc_environment( [[nodiscard]] scriptenv scripting::create_doc_environment(
const scriptenv& parent, const std::string& name const scriptenv& parent, const std::string& name
) { ) {

View File

@ -58,6 +58,7 @@ namespace scripting {
scriptenv get_root_environment(); scriptenv get_root_environment();
scriptenv create_pack_environment(const ContentPack& pack); scriptenv create_pack_environment(const ContentPack& pack);
scriptenv create_environment(const scriptenv& parent);
scriptenv create_doc_environment( scriptenv create_doc_environment(
const scriptenv& parent, const std::string& name const scriptenv& parent, const std::string& name
); );