From 71d3756902c227b4a4ea08f9f468e946bda90007 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Apr 2025 18:31:12 +0300 Subject: [PATCH 1/6] add iframe ui element --- src/graphics/ui/GUI.cpp | 4 ++ src/graphics/ui/GUI.hpp | 1 + src/graphics/ui/elements/InlineFrame.cpp | 51 ++++++++++++++++++++++++ src/graphics/ui/elements/InlineFrame.hpp | 23 +++++++++++ src/graphics/ui/gui_xml.cpp | 18 ++++++++- 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/graphics/ui/elements/InlineFrame.cpp create mode 100644 src/graphics/ui/elements/InlineFrame.hpp diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 828315e6..4de82509 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -368,3 +368,7 @@ Window& GUI::getWindow() { devtools::Editor& GUI::getEditor() { return engine.getEditor(); } + +Engine& GUI::getEngine() { + return engine; +} diff --git a/src/graphics/ui/GUI.hpp b/src/graphics/ui/GUI.hpp index b5c70bdd..c8bb20e2 100644 --- a/src/graphics/ui/GUI.hpp +++ b/src/graphics/ui/GUI.hpp @@ -164,5 +164,6 @@ namespace gui { Input& getInput(); Window& getWindow(); devtools::Editor& getEditor(); + Engine& getEngine(); }; } diff --git a/src/graphics/ui/elements/InlineFrame.cpp b/src/graphics/ui/elements/InlineFrame.cpp new file mode 100644 index 00000000..d81d499f --- /dev/null +++ b/src/graphics/ui/elements/InlineFrame.cpp @@ -0,0 +1,51 @@ +#include "InlineFrame.hpp" +#include "frontend/UiDocument.hpp" +#include "logic/scripting/scripting.hpp" +#include "assets/Assets.hpp" +#include "engine/Engine.hpp" +#include "../GUI.hpp" + +using namespace gui; + +InlineFrame::InlineFrame(GUI& gui) : Container(gui, glm::vec2(1)) {} +InlineFrame::~InlineFrame() = default; + +void InlineFrame::setSrc(const std::string& src) { + this->src = src; + if (document) { + scripting::on_ui_close(document.get(), nullptr); + document = nullptr; + root = nullptr; + } +} + +void InlineFrame::setDocument(const std::shared_ptr& document) { + clear(); + if (document == nullptr) { + return; + } + this->document = document; + this->root = document->getRoot(); + add(root); + + root->setSize(size); + + gui.postRunnable([this]() { + scripting::on_ui_open(this->document.get(), {}); + }); +} + +void InlineFrame::act(float delta) { + if (document || src.empty()) { + return; + } + const auto& assets = *gui.getEngine().getAssets(); + setDocument(assets.getShared(src)); +} + +void InlineFrame::setSize(glm::vec2 size) { + Container::setSize(size); + if (root) { + root->setSize(size); + } +} diff --git a/src/graphics/ui/elements/InlineFrame.hpp b/src/graphics/ui/elements/InlineFrame.hpp new file mode 100644 index 00000000..fd8a6967 --- /dev/null +++ b/src/graphics/ui/elements/InlineFrame.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "Container.hpp" + +class UiDocument; + +namespace gui { + class InlineFrame : public Container { + public: + explicit InlineFrame(GUI& gui); + virtual ~InlineFrame(); + + void setSrc(const std::string& src); + void setDocument(const std::shared_ptr& document); + + void act(float delta) override; + void setSize(glm::vec2 size) override; + private: + std::string src; + std::shared_ptr document; + std::shared_ptr root; + }; +} diff --git a/src/graphics/ui/gui_xml.cpp b/src/graphics/ui/gui_xml.cpp index 31c7bd56..151d1239 100644 --- a/src/graphics/ui/gui_xml.cpp +++ b/src/graphics/ui/gui_xml.cpp @@ -12,6 +12,7 @@ #include "elements/SplitBox.hpp" #include "elements/TrackBar.hpp" #include "elements/Image.hpp" +#include "elements/InlineFrame.hpp" #include "elements/InputBindBox.hpp" #include "elements/InventoryView.hpp" #include "elements/Menu.hpp" @@ -292,7 +293,7 @@ static std::wstring parse_inner_text( return text; } -static std::shared_ptr readLabel( +static std::shared_ptr read_label( const UiXmlReader& reader, const xml::xmlelement& element ) { std::wstring text = parse_inner_text(element, reader.getContext()); @@ -739,11 +740,24 @@ static std::shared_ptr read_page_box( return menu; } +static std::shared_ptr read_iframe( + UiXmlReader& reader, const xml::xmlelement& element +) { + auto& gui = reader.getGUI(); + auto iframe = std::make_shared(gui); + read_container_impl(reader, element, *iframe); + + std::string src = element.attr("src", "").getText(); + iframe->setSrc(src); + return iframe; +} + UiXmlReader::UiXmlReader(gui::GUI& gui, const scriptenv& env) : gui(gui), env(env) { contextStack.emplace(""); add("image", read_image); add("canvas", read_canvas); - add("label", readLabel); + add("iframe", read_iframe); + add("label", read_label); add("panel", read_panel); add("button", read_button); add("textbox", read_text_box); From 7ce97f4abe02c145f44e59b8da81f2a760ad6dbe Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Apr 2025 18:32:14 +0300 Subject: [PATCH 2/6] extract code editor implementation from console.xml --- res/layouts/code_editor.xml | 69 ++++++++ res/layouts/code_editor.xml.lua | 270 +++++++++++++++++++++++++++++++ res/layouts/console.xml | 73 +-------- res/layouts/console.xml.lua | 271 -------------------------------- 4 files changed, 342 insertions(+), 341 deletions(-) create mode 100644 res/layouts/code_editor.xml create mode 100644 res/layouts/code_editor.xml.lua diff --git a/res/layouts/code_editor.xml b/res/layouts/code_editor.xml new file mode 100644 index 00000000..906ff070 --- /dev/null +++ b/res/layouts/code_editor.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layouts/code_editor.xml.lua b/res/layouts/code_editor.xml.lua new file mode 100644 index 00000000..e936aa71 --- /dev/null +++ b/res/layouts/code_editor.xml.lua @@ -0,0 +1,270 @@ +local writeables = {} +local registry = require "core:internal/scripts_registry" +local filenames + +local current_file = { + filename = "", + mutable = nil +} + +local warnings_all = {} +local errors_all = {} + +local warning_id = 0 +local error_id = 0 + +events.on("core:warning", function (wtype, text, traceback) + local full = wtype..": "..text + if table.has(warnings_all, full) then + return + end + local encoded = base64.encode(bjson.tobytes({frames=traceback})) + document.problemsLog:add(gui.template("problem", { + type="warning", + text=full, + traceback=encoded, + id=tostring(warning_id) + })) + warning_id = warning_id + 1 + table.insert(warnings_all, full) +end) + +events.on("core:error", function (msg, traceback) + local _, endindex = string.find(msg, ": ") + local full = "" + for i,frame in ipairs(traceback) do + full = full..frame.source..tostring(frame.currentline) + end + if table.has(errors_all, full) then + return + end + local encoded = base64.encode(bjson.tobytes({frames=traceback})) + document.problemsLog:add(gui.template("problem", { + type="error", + text=msg:sub(endindex), + traceback=encoded, + id=tostring(error_id) + })) + error_id = error_id + 1 + table.insert(errors_all, full) +end) + +local function find_mutable(filename) + local packid = file.prefix(filename) + if packid == "core" then + return + end + local saved = writeables[packid] + if saved then + return saved..":"..file.path(filename) + end + local packinfo = pack.get_info(packid) + if not packinfo then + return + end + local path = packinfo.path + if file.is_writeable(path) then + return file.join(path, file.path(filename)) + end +end + +local function refresh_file_title() + if current_file.filename == "" then + document.title.text = "" + return + end + local edited = document.editor.edited + current_file.modified = edited + document.saveIcon.enabled = edited + document.title.text = gui.str('File')..' - '..current_file.filename + ..(edited and ' *' or '') +end + +function filter_files(text) + local filtered = {} + for _, filename in ipairs(filenames) do + if filename:find(text) then + table.insert(filtered, filename) + end + end + build_files_list(filtered, text) +end + +function on_control_combination(keycode) + if keycode == input.keycode("s") then + save_current_file() + elseif keycode == input.keycode("r") then + run_current_file() + end +end + +function unlock_access() + if current_file.filename == "" then + return + end + pack.request_writeable(file.prefix(current_file.filename), + function(token) + writeables[file.prefix(current_file.filename)] = token + current_file.mutable = token..":"..file.path(current_file.filename) + open_file_in_editor(current_file.filename, 0, current_file.mutable) + end + ) +end + +function run_current_file() + if not current_file.filename then + return + end + local chunk, err = loadstring(document.editor.text, current_file.filename) + clear_output() + if not chunk then + local line, message = err:match(".*:(%d*): (.*)") + document.output:paste( + string.format( + "\n[#FF3030]%s: %s[#FFFFFF]", + gui.str("Error at line %{0}"):gsub("%%{0}", line), message) + ) + return + end + local info = registry.get_info(current_file.filename) + local script_type = info and info.type or "file" + local unit = info and info.unit + save_current_file() + + local func = function() + local stack_size = debug.count_frames() + xpcall(chunk, function(msg) __vc__error(msg, 1, 1, stack_size) end) + end + + local funcs = { + block = block.reload_script, + item = item.reload_script, + world = world.reload_script, + hud = hud.reload_script, + component = entities.reload_component, + module = reload_module, + } + func = funcs[script_type] or func + local output = core.capture_output(function() func(unit) end) + document.output:paste(string.format("\n%s", output)) +end + +function clear_traceback() + local tb_list = document.traceback + tb_list:clear() + tb_list:add("") +end + +function clear_output() + local output = document.output + output.text = "" + output:paste("[#FFFFFF80]"..gui.str("devtools.output").."[#FFFFFF]") +end + +events.on("core:open_traceback", function(traceback_b64) + local traceback = bjson.frombytes(base64.decode(traceback_b64)) + modes:set('debug') + + clear_traceback() + + local tb_list = document.traceback + local srcsize = tb_list.size + for _, frame in ipairs(traceback.frames) do + local callback = "" + local framestr = "" + if frame.what == "C" then + framestr = "C/C++ " + else + framestr = frame.source..":"..tostring(frame.currentline).." " + if file.exists(frame.source) then + callback = string.format( + "open_file_in_editor('%s', %s)", + frame.source, frame.currentline-1 + ) + else + callback = "document.editor.text = 'Could not open source file'" + end + end + if frame.name then + framestr = framestr.."("..tostring(frame.name)..")" + end + local color = "#FFFFFF" + tb_list:add(gui.template("stack_frame", { + location=framestr, + color=color, + callback=callback, + enabled=file.exists(frame.source) + })) + end + tb_list.size = srcsize +end) + +function save_current_file() + if not current_file.mutable then + return + end + file.write(current_file.mutable, document.editor.text) + current_file.modified = false + document.saveIcon.enabled = false + document.title.text = gui.str('File')..' - '..current_file.filename + document.editor.edited = false +end + +function open_file_in_editor(filename, line, mutable) + local editor = document.editor + local source = file.read(filename):gsub('\t', ' ') + editor.scroll = 0 + editor.text = source + editor.focused = true + editor.syntax = file.ext(filename) + if line then + time.post_runnable(function() + editor.caret = editor:linePos(line) + end) + end + document.title.text = gui.str('File') .. ' - ' .. filename + current_file.filename = filename + current_file.mutable = mutable or find_mutable(filename) + document.lockIcon.visible = current_file.mutable == nil + document.editor.editable = current_file.mutable ~= nil + document.saveIcon.enabled = current_file.modified +end + +function build_files_list(filenames, selected) + local files_list = document.filesList + files_list.scroll = 0 + files_list:clear() + + for _, actual_filename in ipairs(filenames) do + local filename = actual_filename + if selected then + filename = filename:gsub(selected, "**"..selected.."**") + end + local parent = file.parent(filename) + local info = registry.get_info(actual_filename) + local icon = "file" + if info then + icon = info.type == "component" and "entity" or info.type + end + files_list:add(gui.template("script_file", { + path = parent .. (parent[#parent] == ':' and '' or '/'), + name = file.name(filename), + icon = icon, + unit = info and info.unit or '', + filename = actual_filename + })) + end +end + +function on_open(mode) + local files_list = document.filesList + + filenames = registry.filenames + table.sort(filenames) + build_files_list(filenames) + + document.editorContainer:setInterval(200, refresh_file_title) + + clear_traceback() + clear_output() +end diff --git a/res/layouts/console.xml b/res/layouts/console.xml index bbfdcf7a..16fbbb0b 100644 --- a/res/layouts/console.xml +++ b/res/layouts/console.xml @@ -22,76 +22,9 @@ markup="md" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @devtools.traceback") -end - -function clear_output() - local output = document.output - output.text = "" - output:paste("[#FFFFFF80]"..gui.str("devtools.output").."[#FFFFFF]") -end - -events.on("core:open_traceback", function(traceback_b64) - local traceback = bjson.frombytes(base64.decode(traceback_b64)) - modes:set('debug') - - clear_traceback() - - local tb_list = document.traceback - local srcsize = tb_list.size - for _, frame in ipairs(traceback.frames) do - local callback = "" - local framestr = "" - if frame.what == "C" then - framestr = "C/C++ " - else - framestr = frame.source..":"..tostring(frame.currentline).." " - if file.exists(frame.source) then - callback = string.format( - "open_file_in_editor('%s', %s)", - frame.source, frame.currentline-1 - ) - else - callback = "document.editor.text = 'Could not open source file'" - end - end - if frame.name then - framestr = framestr.."("..tostring(frame.name)..")" - end - local color = "#FFFFFF" - tb_list:add(gui.template("stack_frame", { - location=framestr, - color=color, - callback=callback, - enabled=file.exists(frame.source) - })) - end - tb_list.size = srcsize -end) - function setup_variables() local pid = hud.get_player() local x,y,z = player.get_pos(pid) @@ -351,9 +93,7 @@ end function set_mode(mode) local show_prompt = mode == 'chat' or mode == 'console' - document.lockIcon.visible = false document.editorRoot.visible = mode == 'debug' - document.editorContainer.visible = mode == 'debug' document.logContainer.visible = mode ~= 'debug' if mode == 'debug' then @@ -378,17 +118,6 @@ function on_open(mode) }, function (mode) set_mode(mode) end, mode or "console") - - local files_list = document.filesList - - filenames = registry.filenames - table.sort(filenames) - build_files_list(filenames) - - document.editorContainer:setInterval(200, refresh_file_title) - - clear_traceback() - clear_output() elseif mode then modes:set(mode) end From e9ec7e3f96fb4bcf91a09c7ef68c72a9da9445b1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Apr 2025 20:12:38 +0300 Subject: [PATCH 3/6] small visual fix --- src/graphics/ui/GUI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 4de82509..dc379a68 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -280,7 +280,7 @@ void GUI::draw(const DrawContext& pctx, const Assets& assets) { auto size = node->getSize(); batch2D->setColor(0, 255, 255); - batch2D->lineRect(parentPos.x, parentPos.y, size.x-1, size.y-1); + batch2D->lineRect(parentPos.x+1, parentPos.y, size.x-2, size.y-1); node = node->getParent(); } From 8b0f350b6955e3e68909eb9dacf48b2580af67e1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Apr 2025 20:25:18 +0300 Subject: [PATCH 4/6] update doc/*/xml-ui-layouts.md --- doc/en/xml-ui-layouts.md | 6 ++++++ doc/ru/xml-ui-layouts.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/doc/en/xml-ui-layouts.md b/doc/en/xml-ui-layouts.md index 02f1992f..3f22c748 100644 --- a/doc/en/xml-ui-layouts.md +++ b/doc/en/xml-ui-layouts.md @@ -154,6 +154,12 @@ The key code for comparison can be obtained via `input.keycode("key_name")` - `supplier` - Lua function - value supplier - `change-on-release` - Call consumer on trackbar release only. Type: boolean. Default: false +## Inline frame - *iframe* + +Container for embedding an external document. Content is scaling to the iframe size. + +- `src` - document id in the format `pack:name` (`pack/layouts/name.xml`) + # Inventory elements ## *inventory* diff --git a/doc/ru/xml-ui-layouts.md b/doc/ru/xml-ui-layouts.md index 0a1cb25e..df8e08e3 100644 --- a/doc/ru/xml-ui-layouts.md +++ b/doc/ru/xml-ui-layouts.md @@ -155,6 +155,12 @@ - `supplier` - lua функция-поставщик значения - `change-on-release` - Вызов функции-приемника (consumer) происходит только тогда, когда пользователь отпускает указатель. Тип: логический. По-умолчанию: false +## Рамка встраивания - *iframe* + +Контейнер для встраивания внешнего документа. Масштабирует содержимое под свой размер. + +- `src` - id документа в формате `пак:имя` (`пак/layouts/имя.xml`) + # Элементы инвентаря ## Инвентарь - *inventory* From f21d9d0a25d869f2daf0d6c00c1f414fe0c79bd3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Apr 2025 20:34:30 +0300 Subject: [PATCH 5/6] add iframe 'src' scripting property --- src/graphics/ui/elements/InlineFrame.cpp | 4 ++++ src/graphics/ui/elements/InlineFrame.hpp | 2 ++ src/logic/scripting/lua/libs/libgui.cpp | 20 ++++++++------------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/graphics/ui/elements/InlineFrame.cpp b/src/graphics/ui/elements/InlineFrame.cpp index d81d499f..cd01d45b 100644 --- a/src/graphics/ui/elements/InlineFrame.cpp +++ b/src/graphics/ui/elements/InlineFrame.cpp @@ -49,3 +49,7 @@ void InlineFrame::setSize(glm::vec2 size) { root->setSize(size); } } + +const std::string& InlineFrame::getSrc() const { + return src; +} diff --git a/src/graphics/ui/elements/InlineFrame.hpp b/src/graphics/ui/elements/InlineFrame.hpp index fd8a6967..41ad7345 100644 --- a/src/graphics/ui/elements/InlineFrame.hpp +++ b/src/graphics/ui/elements/InlineFrame.hpp @@ -15,6 +15,8 @@ namespace gui { void act(float delta) override; void setSize(glm::vec2 size) override; + + const std::string& getSrc() const; private: std::string src; std::shared_ptr document; diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 7c891c88..3fb5fcde 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -13,6 +13,7 @@ #include "graphics/ui/elements/Panel.hpp" #include "graphics/ui/elements/TextBox.hpp" #include "graphics/ui/elements/TrackBar.hpp" +#include "graphics/ui/elements/InlineFrame.hpp" #include "graphics/ui/gui_util.hpp" #include "graphics/ui/markdown.hpp" #include "graphics/core/Font.hpp" @@ -330,6 +331,8 @@ static int p_get_markup(UINode* node, lua::State* L) { static int p_get_src(UINode* node, lua::State* L) { if (auto image = dynamic_cast(node)) { return lua::pushstring(L, image->getTexture()); + } else if (auto iframe = dynamic_cast(node)) { + return lua::pushstring(L, iframe->getSrc()); } return 0; } @@ -644,6 +647,8 @@ static void p_set_markup(UINode* node, lua::State* L, int idx) { static void p_set_src(UINode* node, lua::State* L, int idx) { if (auto image = dynamic_cast(node)) { image->setTexture(lua::require_string(L, idx)); + } else if (auto iframe = dynamic_cast(node)) { + iframe->setSrc(lua::require_string(L, idx)); } } static void p_set_value(UINode* node, lua::State* L, int idx) { @@ -704,10 +709,10 @@ static void p_set_inventory(UINode* node, lua::State* L, int idx) { } } static void p_set_focused( - const std::shared_ptr& node, lua::State* L, int idx + UINode* node, lua::State* L, int idx ) { if (lua::toboolean(L, idx) && !node->isFocused()) { - engine->getGUI().setFocus(node); + engine->getGUI().setFocus(node->shared_from_this()); } else if (node->isFocused()) { node->defocus(); } @@ -771,21 +776,12 @@ static int l_gui_setattr(lua::State* L) { {"page", p_set_page}, {"inventory", p_set_inventory}, {"cursor", p_set_cursor}, + {"focused", p_set_focused}, }; auto func = setters.find(attr); if (func != setters.end()) { func->second(node.get(), L, 4); } - static const std::unordered_map< - std::string_view, - std::function, lua::State*, int)>> - setters2 { - {"focused", p_set_focused}, - }; - auto func2 = setters2.find(attr); - if (func2 != setters2.end()) { - func2->second(node, L, 4); - } return 0; } From 64375c4b2870ce09eaadf2d03d07e6f9487e6c9c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Apr 2025 20:38:58 +0300 Subject: [PATCH 6/6] update doc/*/scripting/ui.md --- doc/en/scripting/ui.md | 5 +++++ doc/ru/scripting/ui.md | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/en/scripting/ui.md b/doc/en/scripting/ui.md index c649fadc..66315f29 100644 --- a/doc/en/scripting/ui.md +++ b/doc/en/scripting/ui.md @@ -195,6 +195,11 @@ Here, *color* can be specified in the following ways: | data:set_data(data: table) | replaces pixel data (width * height * 4 numbers) | | data:create_texture(name: str) | creates and shares texture to renderer | +## Inline frame (iframe) + +| Name | Type | Read | Write | Description | +|----------|--------|------|-------|-----------------------------| +| src | string | yes | yes | id of the embedded document | ## Inventory diff --git a/doc/ru/scripting/ui.md b/doc/ru/scripting/ui.md index 918b77af..cdc7a9bd 100644 --- a/doc/ru/scripting/ui.md +++ b/doc/ru/scripting/ui.md @@ -196,11 +196,17 @@ document["worlds-panel"]:clear() | data:set_data(data: table) | заменяет данные пикселей (ширина * высота * 4 чисел) | | data:create_texture(name: str) | создаёт и делится текстурой с рендерером | -## Inventory (inventory) +## Рамка встраивания (iframe) + +| Название | Тип | Чтение | Запись | Описание | +|----------|--------|--------|--------|----------------------------| +| src | string | да | да | id встраиваемого документа | + +## Инвентарь (inventory) Свойства: | Название | Тип | Чтение | Запись | Описание | -| --------- | --- | ------ | ------ | ----------------------------------------- | +|-----------|-----|--------|--------|-------------------------------------------| | inventory | int | да | да | id инвентаря, к которому привязан элемент |