diff --git a/doc/en/scripting/ui.md b/doc/en/scripting/ui.md index 4cac91b7..c6da89c4 100644 --- a/doc/en/scripting/ui.md +++ b/doc/en/scripting/ui.md @@ -79,6 +79,7 @@ Properties: | hint | string | yes | yes | text to display when nothing is entered | | caret | int | yes | yes | carriage position. `textbox.caret = -1` will set the position to the end of the text | | editable | bool | yes | yes | text mutability | +| edited | bool | yes | yes\* | is text edited since the last set / edited status reset | | multiline | bool | yes | yes | multiline support | | lineNumbers | bool | yes | yes | display line numbers | | textWrap | bool | yes | yes | automatic text wrapping (only with multiline: "true") | @@ -87,6 +88,8 @@ Properties: | syntax | string | yes | yes | syntax highlighting ("lua" - Lua) | | markup | string | yes | yes | text markup language ("md" - Markdown) | +\* - false only + Methods: | Method | Description | diff --git a/doc/en/xml-ui-layouts.md b/doc/en/xml-ui-layouts.md index 6f687328..64579db1 100644 --- a/doc/en/xml-ui-layouts.md +++ b/doc/en/xml-ui-layouts.md @@ -110,6 +110,8 @@ Inner text - initially entered text - `supplier` - text supplier (called every frame) - `consumer` - lua function that receives the entered text. Called only when input is complete - `sub-consumer` - lua function-receiver of the input text. Called during text input or deletion. +- `oncontrolkey` - lua function called for combinations of the form (Ctrl + ?). The codepoint of the second key is given as the first argument. +The key code for comparison can be obtained via `input.keycode("key_name")` - `autoresize` - automatic change of element size (default - false). Does not affect font size. - `multiline` - allows display of multiline text. - `text-wrap` - allows automatic text wrapping (works only with multiline: "true") diff --git a/doc/ru/scripting/ui.md b/doc/ru/scripting/ui.md index 137ff57b..62697c8f 100644 --- a/doc/ru/scripting/ui.md +++ b/doc/ru/scripting/ui.md @@ -79,6 +79,7 @@ document["worlds-panel"]:clear() | hint | string | да | да | текст, отображаемый, когда ничего не введено | | caret | int | да | да | позиция каретки. `textbox.caret = -1` установит позицию в конец текста | | editable | bool | да | да | изменяемость текста | +| edited | bool | да | да\* | был ли изменён текст с последней установки/сброса свойства | | multiline | bool | да | да | поддержка многострочности | | lineNumbers | bool | да | да | отображение номеров строк | | textWrap | bool | да | да | автоматический перенос текста (только при multiline: "true") | @@ -87,6 +88,8 @@ document["worlds-panel"]:clear() | syntax | string | да | да | подсветка синтаксиса ("lua" - Lua) | | markup | string | да | да | язык разметки текста ("md" - Markdown) | +\* - только false + Методы: | Метод | Описание | diff --git a/doc/ru/xml-ui-layouts.md b/doc/ru/xml-ui-layouts.md index 30c17315..e0524793 100644 --- a/doc/ru/xml-ui-layouts.md +++ b/doc/ru/xml-ui-layouts.md @@ -111,6 +111,8 @@ - `supplier` - поставщик текста (вызывается каждый кадр) - `consumer` - lua функция-приемник введенного текста. Вызывается только при завершении ввода - `sub-consumer` - lua функция-приемник вводимого текста. Вызывается во время ввода или удаления текста. +- `oncontrolkey` - lua функция вызываемая для сочетаний вида (Ctrl + ?). На вход подаётся числовой код второй клавиши. +Код клавиши для сравнения можно получить через `input.keycode("имя_клавиши")` - `autoresize` - автоматическое изменение размера элемента (по-умолчанию - false). Не влияет на размер шрифта. - `multiline` - разрешает отображение многострочного текста. - `text-wrap` - разрешает автоматический перенос текста (работает только при multiline: "true") diff --git a/res/layouts/console.xml b/res/layouts/console.xml index 18d72163..bc5fb343 100644 --- a/res/layouts/console.xml +++ b/res/layouts/console.xml @@ -2,17 +2,14 @@ + size-func="gui.get_viewport()[1],30"> - - - + size-func="unpack(vec2.add(gui.get_viewport(), {-450,-100}))"> - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %s: %s", + 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:add( + string.format( + "", + output) + ) +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.text = source + editor.focused = true + 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 clear_traceback() + local tb_list = document.traceback + tb_list:clear() + tb_list:add("") +end + +function clear_output() + local output = document.output + output:clear() + output:add("") +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 - tb_list:clear() - tb_list:add("") for _, frame in ipairs(traceback.frames) do local callback = "" local framestr = "" @@ -62,23 +241,12 @@ events.on("core:open_traceback", function(traceback_b64) framestr = frame.source..":"..tostring(frame.currentline).." " if file.exists(frame.source) then callback = string.format( - "local editor = document.editor ".. - "local source = file.read('%s'):gsub('\t', ' ') ".. - "editor.text = source ".. - "editor.focused = true ".. - "time.post_runnable(function()".. - "editor.caret = editor:linePos(%s) ".. - "end)", + "open_file_in_editor('%s', %s)", frame.source, frame.currentline-1 ) else callback = "document.editor.text = 'Could not open source file'" end - callback = string.format( - "%s document.title.text = gui.str('File')..' - %s'", - callback, - frame.source - ) end if frame.name then framestr = framestr.."("..tostring(frame.name)..")" @@ -184,7 +352,8 @@ end function set_mode(mode) local show_prompt = mode == 'chat' or mode == 'console' - document.title.text = "" + document.lockIcon.visible = false + document.editorRoot.visible = mode == 'debug' document.editorContainer.visible = mode == 'debug' document.logContainer.visible = mode ~= 'debug' @@ -194,7 +363,6 @@ function set_mode(mode) document.root.color = {0, 0, 0, 128} end - document.traceback.visible = mode == 'debug' document.prompt.visible = show_prompt if show_prompt then document.prompt.focused = true @@ -211,6 +379,17 @@ 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 diff --git a/res/layouts/templates/problem.xml b/res/layouts/templates/problem.xml index 17085392..f4382dbb 100644 --- a/res/layouts/templates/problem.xml +++ b/res/layouts/templates/problem.xml @@ -1,9 +1,7 @@ - - - + diff --git a/res/layouts/templates/script_file.xml b/res/layouts/templates/script_file.xml new file mode 100644 index 00000000..94e1afd2 --- /dev/null +++ b/res/layouts/templates/script_file.xml @@ -0,0 +1,12 @@ + + + + diff --git a/res/modules/internal/scripts_registry.lua b/res/modules/internal/scripts_registry.lua new file mode 100644 index 00000000..de685224 --- /dev/null +++ b/res/modules/internal/scripts_registry.lua @@ -0,0 +1,74 @@ +local export = {} + +local function collect_components(dirname, dest) + if file.isdir(dirname) then + local files = file.list(dirname) + for i, filename in ipairs(files) do + if file.ext(filename) == "lua" then + table.insert(dest, filename) + export.classification[filename] = { + type="component", + unit=file.prefix(filename)..":"..file.stem(filename) + } + end + end + end +end + +local function collect_scripts(dirname, dest, ismodule) + if file.isdir(dirname) then + local files = file.list(dirname) + for i, filename in ipairs(files) do + if file.name(filename) == "components" and not ismodule then + collect_components(filename, dest) + elseif file.isdir(filename) then + collect_scripts(filename, dest) + elseif file.ext(filename) == "lua" then + table.insert(dest, filename) + end + end + end +end + +local function load_scripts_list() + local packs = pack.get_installed() + for _, packid in ipairs(packs) do + collect_scripts(packid..":modules", export.filenames, true) + end + for _, filename in ipairs(export.filenames) do + export.classification[filename] = { + type="module", + unit=file.join(file.parent(file.prefix(filename)..":".. + filename:sub(filename:find("/")+1)), + file.stem(filename)) + } + end + for _, packid in ipairs(packs) do + collect_scripts(packid..":scripts", export.filenames, false) + end +end + +function export.build_classification() + local classification = {} + for id, props in pairs(block.properties) do + classification[props["script-file"]] = {type="block", unit=block.name(id)} + end + for id, props in pairs(item.properties) do + classification[props["script-file"]] = {type="item", unit=item.name(id)} + end + local packs = pack.get_installed() + for _, packid in ipairs(packs) do + classification[packid..":scripts/world.lua"] = {type="world", unit=packid} + classification[packid..":scripts/hud.lua"] = {type="hud", unit=packid} + end + export.classification = classification + export.filenames = {} + + load_scripts_list() +end + +function export.get_info(filename) + return export.classification[filename] +end + +return export diff --git a/res/preload.json b/res/preload.json index c4e60cad..1cfd0747 100644 --- a/res/preload.json +++ b/res/preload.json @@ -24,7 +24,18 @@ "misc/snow", "gui/check_mark", "gui/left_arrow", - "gui/right_arrow" + "gui/right_arrow", + "gui/lock", + "gui/save", + "gui/block", + "gui/item", + "gui/file", + "gui/module", + "gui/play", + "gui/info", + "gui/world", + "gui/hud", + "gui/entity" ], "fonts": [ { diff --git a/res/scripts/post_content.lua b/res/scripts/post_content.lua index 8725ed97..85d7b28a 100644 --- a/res/scripts/post_content.lua +++ b/res/scripts/post_content.lua @@ -7,7 +7,7 @@ local names = { "hidden", "draw-group", "picking-item", "surface-replacement", "script-name", "ui-layout", "inventory-size", "tick-interval", "overlay-texture", "translucent", "fields", "particles", "icon-type", "icon", "placing-block", - "stack-size", "name" + "stack-size", "name", "script-file" } for name, _ in pairs(user_props) do table.insert(names, name) @@ -61,3 +61,6 @@ end cache_names(block) cache_names(item) + +local scripts_registry = require "core:internal/scripts_registry" +scripts_registry.build_classification() diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index e2e4d3c7..1366966a 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -429,6 +429,18 @@ function file.readlines(path) return lines end +function debug.count_frames() + local frames = 1 + while true do + local info = debug.getinfo(frames) + if info then + frames = frames + 1 + else + return frames - 1 + end + end +end + function debug.get_traceback(start) local frames = {} local n = 2 + (start or 0) @@ -463,6 +475,35 @@ function on_deprecated_call(name, alternatives) end end +function reload_module(name) + local prefix, name = parse_path(name) + local path = prefix..":modules/"..name..".lua" + + local previous = package.loaded[path] + if not previous then + debug.log("attempt to reload non-loaded module "..name.." ("..path..")") + return + end + local script, err = load(file.read(path), path) + if script == nil then + error(err) + end + local result = script() + if not result then + return + end + for i, value in ipairs(result) do + previous[i] = value + end + local copy = table.copy(result) + for key, value in pairs(result) do + result[key] = nil + end + for key, value in pairs(copy) do + previous[key] = value + end +end + -- Load script with caching -- -- path - script path `contentpack:filename`. @@ -513,9 +554,13 @@ function __scripts_cleanup() end end -function __vc__error(msg, frame) +function __vc__error(msg, frame, n, lastn) if events then - events.emit("core:error", msg, debug.get_traceback(1)) + local frames = debug.get_traceback(1) + events.emit( + "core:error", msg, + table.sub(frames, 1 + (n or 0), lastn and #frames-lastn) + ) end return debug.traceback(msg, frame) end @@ -543,3 +588,23 @@ end function file.prefix(path) return path:match("^([^:]+)") end + +function file.parent(path) + local dir = path:match("(.*)/") + if not dir then + return file.prefix(path)..":" + end + return dir +end + +function file.path(path) + local pos = path:find(':') + return path:sub(pos + 1) +end + +function file.join(a, b) + if a[#a] == ':' then + return a .. b + end + return a .. "/" .. b +end diff --git a/res/texts/en_US.txt b/res/texts/en_US.txt index 732e2d66..57a6cb6b 100644 --- a/res/texts/en_US.txt +++ b/res/texts/en_US.txt @@ -11,7 +11,9 @@ world.delete-confirm=Do you want to delete world forever? world.generators.default=Default world.generators.flat=Flat +editor.info.tooltip=CTRL+S - Save\nCTRL+R - Run\nCTRL+Z - Undo\nCTRL+Y - Redo devtools.traceback=Traceback (most recent call first) +devtools.output=Output # Tooltips graphics.gamma.tooltip=Lighting brightness curve diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index 80a7ba6a..65db4507 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -19,8 +19,16 @@ Problems=Проблемы Monitor=Мониторинг Debug=Отладка File=Файл +Read only=Только для чтения +Save=Сохранить +Grant %{0} pack modification permission?=Выдать разрешение на модификацию пака %{0}? +Error at line %{0}=Ошибка на строке %{0} +Run=Запустить +editor.info.tooltip=CTRL+S - Сохранить\nCTRL+R - Запустить\nCTRL+Z - Отменить\nCTRL+Y - Повторить devtools.traceback=Стек вызовов (от последнего) +devtools.output=Вывод + error.pack-not-found=Не удалось найти пакет error.dependency-not-found=Используемая зависимость не найдена pack.remove-confirm=Удалить весь поставляемый паком/паками контент из мира (безвозвратно)? diff --git a/res/textures/gui/block.png b/res/textures/gui/block.png new file mode 100644 index 00000000..dfcc239a Binary files /dev/null and b/res/textures/gui/block.png differ diff --git a/res/textures/gui/entity.png b/res/textures/gui/entity.png new file mode 100644 index 00000000..53207e8b Binary files /dev/null and b/res/textures/gui/entity.png differ diff --git a/res/textures/gui/file.png b/res/textures/gui/file.png new file mode 100644 index 00000000..3d5b5c21 Binary files /dev/null and b/res/textures/gui/file.png differ diff --git a/res/textures/gui/hud.png b/res/textures/gui/hud.png new file mode 100644 index 00000000..4d32d616 Binary files /dev/null and b/res/textures/gui/hud.png differ diff --git a/res/textures/gui/info.png b/res/textures/gui/info.png new file mode 100644 index 00000000..a7bb7030 Binary files /dev/null and b/res/textures/gui/info.png differ diff --git a/res/textures/gui/item.png b/res/textures/gui/item.png new file mode 100644 index 00000000..8714b10f Binary files /dev/null and b/res/textures/gui/item.png differ diff --git a/res/textures/gui/lock.png b/res/textures/gui/lock.png new file mode 100644 index 00000000..10c877a3 Binary files /dev/null and b/res/textures/gui/lock.png differ diff --git a/res/textures/gui/module.png b/res/textures/gui/module.png new file mode 100644 index 00000000..84922d72 Binary files /dev/null and b/res/textures/gui/module.png differ diff --git a/res/textures/gui/play.png b/res/textures/gui/play.png new file mode 100644 index 00000000..2442b02c Binary files /dev/null and b/res/textures/gui/play.png differ diff --git a/res/textures/gui/save.png b/res/textures/gui/save.png new file mode 100644 index 00000000..8fc66b5a Binary files /dev/null and b/res/textures/gui/save.png differ diff --git a/res/textures/gui/world.png b/res/textures/gui/world.png new file mode 100644 index 00000000..2f0320c3 Binary files /dev/null and b/res/textures/gui/world.png differ diff --git a/src/constants.hpp b/src/constants.hpp index 9337513f..1accd619 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -68,3 +68,5 @@ inline const std::string LAYOUTS_FOLDER = "layouts"; inline const std::string SOUNDS_FOLDER = "sounds"; inline const std::string MODELS_FOLDER = "models"; inline const std::string SKELETONS_FOLDER = "skeletons"; + +inline const std::string FONT_DEFAULT = "normal"; diff --git a/src/content/Content.cpp b/src/content/Content.cpp index 1cc2fdd7..ce92be18 100644 --- a/src/content/Content.cpp +++ b/src/content/Content.cpp @@ -79,6 +79,14 @@ const ContentPackRuntime* Content::getPackRuntime(const std::string& id) const { return found->second.get(); } +ContentPackRuntime* Content::getPackRuntime(const std::string& id) { + auto found = packs.find(id); + if (found == packs.end()) { + return nullptr; + } + return found->second.get(); +} + const UptrsMap& Content::getBlockMaterials() const { return blockMaterials; } diff --git a/src/content/Content.hpp b/src/content/Content.hpp index b95ef247..94650b25 100644 --- a/src/content/Content.hpp +++ b/src/content/Content.hpp @@ -121,6 +121,14 @@ public: return *found->second; } + T& require(const std::string& id) { + const auto& found = defs.find(id); + if (found == defs.end()) { + throw std::runtime_error("missing content unit " + id); + } + return *found->second; + } + const auto& getDefs() const { return defs; } @@ -240,6 +248,7 @@ public: const rigging::SkeletonConfig* getSkeleton(const std::string& id) const; const BlockMaterial* findBlockMaterial(const std::string& id) const; const ContentPackRuntime* getPackRuntime(const std::string& id) const; + ContentPackRuntime* getPackRuntime(const std::string& id); const UptrsMap& getBlockMaterials() const; const UptrsMap& getPacks() const; diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index c57a7ad8..caab68976 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -94,8 +94,9 @@ std::unique_ptr ContentBuilder::build() { def->rt.surfaceReplacement = content->blocks.require(def->surfaceReplacement).rt.id; if (def->properties == nullptr) { def->properties = dv::object(); - def->properties["name"] = def->name; } + def->properties["name"] = def->name; + def->properties["script-file"] = def->scriptFile; } for (ItemDef* def : itemDefsIndices) { @@ -104,6 +105,7 @@ std::unique_ptr ContentBuilder::build() { def->properties = dv::object(); } def->properties["name"] = def->name; + def->properties["script-file"] = def->scriptFile; } for (auto& [name, def] : content->generators.getDefs()) { diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index 9b14bc86..3fb2b8f6 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -394,6 +394,7 @@ void ContentLoader::loadBlock( if (def.hidden && def.pickingItem == def.name + BLOCK_ITEM_SUFFIX) { def.pickingItem = CORE_EMPTY; } + def.scriptFile = pack->id + ":scripts/" + def.scriptName + ".lua"; } void ContentLoader::loadItem( @@ -452,6 +453,8 @@ void ContentLoader::loadItem( def.emission[1] = emissionarr[1].asNumber(); def.emission[2] = emissionarr[2].asNumber(); } + + def.scriptFile = pack->id + ":scripts/" + def.scriptName + ".lua"; } void ContentLoader::loadEntity( @@ -850,25 +853,54 @@ void ContentLoader::load() { } template -static void load_scripts(Content& content, ContentUnitDefs& units) { - for (const auto& [name, def] : units.getDefs()) { - size_t pos = name.find(':'); - if (pos == std::string::npos) { - throw std::runtime_error("invalid content unit name"); - } - const auto runtime = content.getPackRuntime(name.substr(0, pos)); - const auto& pack = runtime->getInfo(); - const auto& folder = pack.folder; - auto scriptfile = folder / ("scripts/" + def->scriptName + ".lua"); - if (io::is_regular_file(scriptfile)) { - scripting::load_content_script( - runtime->getEnvironment(), - name, - scriptfile, - pack.id + ":scripts/" + def->scriptName + ".lua", - def->rt.funcsset - ); - } +static void load_script(const Content& content, T& def) { + const auto& name = def.name; + size_t pos = name.find(':'); + if (pos == std::string::npos) { + throw std::runtime_error("invalid content unit name"); + } + const auto runtime = content.getPackRuntime(name.substr(0, pos)); + const auto& pack = runtime->getInfo(); + const auto& folder = pack.folder; + auto scriptfile = folder / ("scripts/" + def.scriptName + ".lua"); + if (io::is_regular_file(scriptfile)) { + scripting::load_content_script( + runtime->getEnvironment(), + name, + scriptfile, + def.scriptFile, + def.rt.funcsset + ); + } +} + +template +static void load_scripts(const Content& content, ContentUnitDefs& units) { + for (const auto& [_, def] : units.getDefs()) { + load_script(content, *def); + } +} + +void ContentLoader::reloadScript(const Content& content, Block& block) { + load_script(content, block); +} + +void ContentLoader::reloadScript(const Content& content, ItemDef& item) { + load_script(content, item); +} + +void ContentLoader::loadWorldScript(ContentPackRuntime& runtime) { + const auto& pack = runtime.getInfo(); + const auto& folder = pack.folder; + io::path scriptFile = folder / "scripts/world.lua"; + if (io::is_regular_file(scriptFile)) { + scripting::load_world_script( + runtime.getEnvironment(), + pack.id, + scriptFile, + pack.id + ":scripts/world.lua", + runtime.worldfuncsset + ); } } @@ -881,16 +913,8 @@ void ContentLoader::loadScripts(Content& content) { const auto& folder = pack.folder; // Load main world script - io::path scriptFile = folder / "scripts/world.lua"; - if (io::is_regular_file(scriptFile)) { - scripting::load_world_script( - runtime->getEnvironment(), - pack.id, - scriptFile, - pack.id + ":scripts/world.lua", - runtime->worldfuncsset - ); - } + loadWorldScript(*runtime); + // Load entity components io::path componentsDir = folder / "scripts/components"; foreach_file(componentsDir, [&pack](const io::path& file) { diff --git a/src/content/ContentLoader.hpp b/src/content/ContentLoader.hpp index f1135ee3..eeedf11b 100644 --- a/src/content/ContentLoader.hpp +++ b/src/content/ContentLoader.hpp @@ -77,4 +77,7 @@ public: void load(); static void loadScripts(Content& content); + static void loadWorldScript(ContentPackRuntime& pack); + static void reloadScript(const Content& content, Block& block); + static void reloadScript(const Content& content, ItemDef& item); }; diff --git a/src/delegates.hpp b/src/delegates.hpp index 0e670b41..f583f951 100644 --- a/src/delegates.hpp +++ b/src/delegates.hpp @@ -15,6 +15,7 @@ using wstringsupplier = std::function; using doublesupplier = std::function; using boolsupplier = std::function; using vec2supplier = std::function; +using key_handler = std::function; using stringconsumer = std::function; using wstringconsumer = std::function; diff --git a/src/devtools/actions.hpp b/src/devtools/actions.hpp new file mode 100644 index 00000000..fffe9d67 --- /dev/null +++ b/src/devtools/actions.hpp @@ -0,0 +1,164 @@ +#pragma once + +#include +#include + +class Action { +public: + virtual ~Action() = default; + + virtual void apply() = 0; + virtual void revert() = 0; +}; + +class InversedAction : public Action { +public: + InversedAction(std::unique_ptr action) : action(std::move(action)) {} + + void apply() override { + action->revert(); + } + + void revert() override { + action->apply(); + } + private: + std::unique_ptr action; +}; + +class CombinedAction : public Action { +public: + CombinedAction(std::vector> actions) + : actions(std::move(actions)) { + } + + void apply() override { + for (auto& action : actions) { + action->apply(); + } + } + + void revert() override { + for (int i = actions.size() - 1; i >= 0; i--) { + actions[i]->revert(); + } + } +private: + std::vector> actions; +}; + +class ActionsHistory { +public: + ActionsHistory() {}; + + /// @brief Remove all actions available to redo + void clearRedo() { + if (actionPtr < actions.size()) { + actions.erase(actions.begin() + actionPtr, actions.end()); + } + } + + /// @brief Store action without applying + void store(std::unique_ptr action, bool reverse=false) { + if (lock) { + return; + } + if (reverse) { + action = std::make_unique(std::move(action)); + } + clearRedo(); + actions.emplace_back(std::move(action)); + actionPtr++; + } + + /// @brief Apply action and store it + void apply(std::unique_ptr action) { + if (lock) { + return; + } + clearRedo(); + lock = true; + action->apply(); + lock = false; + actions.emplace_back(std::move(action)); + actionPtr++; + } + + /// @brief Revert the last action + /// @return true if any action reverted + bool undo() { + if (lock || actionPtr == 0) { + return false; + } + auto& action = actions[--actionPtr]; + lock = true; + action->revert(); + lock = false; + return true; + } + + /// @brief Revert the last action + /// @return true if any action reapplied + bool redo() { + if (lock || actionPtr == actions.size()) { + return false; + } + auto& action = actions[actionPtr++]; + lock = true; + action->apply(); + lock = false; + return true; + } + + /// @brief Clear history without reverting actions + void clear() { + actionPtr = 0; + actions.clear(); + } + + /// @brief Squash last n actions into one CombinedAction + /// @param n number of actions to squash + void squash(ptrdiff_t n) { + if (n < 2) { + return; + } + n = std::min(n, static_cast(actionPtr)); + std::vector> squashing; + for (size_t i = actionPtr - n; i < actionPtr; i++) { + squashing.emplace_back(std::move(actions[i])); + } + actions.erase(actions.begin() + actionPtr - n, actions.end()); + actionPtr -= n; + store(std::make_unique(std::move(squashing))); + } + + size_t size() const { + return actionPtr; + } + + /// @brief On destruction squashing actions stored since initialization + struct Combination { + ActionsHistory& history; + size_t historySize; + + Combination(ActionsHistory& history) + : history(history), historySize(history.size()) { + } + + Combination(const Combination&) = delete; + + Combination(Combination&&) = default; + + ~Combination() { + history.squash(history.size() - historySize); + } + }; + + Combination beginCombination() { + return Combination(*this); + } +private: + std::vector> actions; + size_t actionPtr = 0; + bool lock = false; +}; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 80d774be..5778ed98 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -163,6 +163,9 @@ void Engine::updateHotkeys() { if (Events::jpressed(keycode::F2)) { saveScreenshot(); } + if (Events::jpressed(keycode::F8)) { + gui->toggleDebug(); + } if (Events::jpressed(keycode::F11)) { settings.display.fullscreen.toggle(); } @@ -481,6 +484,10 @@ const Content* Engine::getContent() const { return content.get(); } +Content* Engine::getWriteableContent() { + return content.get(); +} + std::vector Engine::getAllContentPacks() { auto packs = getContentPacks(); packs.insert(packs.begin(), ContentPack::createCore(paths)); diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index b05881f6..ccfabeed 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -151,6 +151,8 @@ public: /// @brief Get current Content instance const Content* getContent() const; + Content* getWriteableContent(); + /// @brief Get selected content packs std::vector& getContentPacks(); diff --git a/src/frontend/hud.cpp b/src/frontend/hud.cpp index 1684799f..48a699f1 100644 --- a/src/frontend/hud.cpp +++ b/src/frontend/hud.cpp @@ -262,7 +262,7 @@ void Hud::updateHotbarControl() { } } -void Hud::updateWorldGenDebugVisualization() { +void Hud::updateWorldGenDebug() { auto& level = frontend.getLevel(); const auto& chunks = *player.chunks; auto generator = @@ -314,7 +314,9 @@ void Hud::update(bool visible) { const auto& chunks = *player.chunks; const auto& menu = gui.getMenu(); - debugPanel->setVisible(debug && visible); + debugPanel->setVisible( + debug && visible && !(inventoryOpen && inventoryView == nullptr) + ); if (!visible && inventoryOpen) { closeInventory(); @@ -358,7 +360,7 @@ void Hud::update(bool visible) { debugMinimap->setVisible(debug && showGeneratorMinimap); if (debug && showGeneratorMinimap) { - updateWorldGenDebugVisualization(); + updateWorldGenDebug(); } } diff --git a/src/frontend/hud.hpp b/src/frontend/hud.hpp index a978e282..3522cd34 100644 --- a/src/frontend/hud.hpp +++ b/src/frontend/hud.hpp @@ -135,7 +135,7 @@ class Hud : public util::ObjectsKeeper { void dropExchangeSlot(); void showExchangeSlot(); - void updateWorldGenDebugVisualization(); + void updateWorldGenDebug(); public: Hud(Engine& engine, LevelFrontend& frontend, Player& player); ~Hud(); diff --git a/src/graphics/core/Batch2D.cpp b/src/graphics/core/Batch2D.cpp index b7ccf5ff..73ac9be2 100644 --- a/src/graphics/core/Batch2D.cpp +++ b/src/graphics/core/Batch2D.cpp @@ -142,7 +142,7 @@ void Batch2D::rect( bool flippedY, glm::vec4 tint ) { - if (index + 6*B2D_VERTEX_SIZE >= capacity) { + if (index + 6 * B2D_VERTEX_SIZE >= capacity) { flush(); } setPrimitive(DrawPrimitive::triangle); @@ -230,6 +230,11 @@ void Batch2D::rect( } void Batch2D::lineRect(float x, float y, float w, float h) { + if (index + 8 * B2D_VERTEX_SIZE >= capacity) { + flush(); + } + setPrimitive(DrawPrimitive::line); + vertex(x, y, 0.0f, 0.0f, color.r, color.g, color.b, color.a); vertex(x, y+h, 0.0f, 1.0f, color.r, color.g, color.b, color.a); diff --git a/src/graphics/core/Batch2D.hpp b/src/graphics/core/Batch2D.hpp index 5610f603..8db99d99 100644 --- a/src/graphics/core/Batch2D.hpp +++ b/src/graphics/core/Batch2D.hpp @@ -48,10 +48,14 @@ public: void sprite(float x, float y, float w, float h, float skew, int atlasRes, int index, glm::vec4 tint); void point(float x, float y, float r, float g, float b, float a); - void setColor(glm::vec4 color) { + void setColor(const glm::vec4& color) { this->color = color; } + void setColor(int r, int g, int b, int a=255) { + this->color = glm::vec4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); + } + void resetColor() { this->color = glm::vec4(1.0f); } diff --git a/src/graphics/core/Font.cpp b/src/graphics/core/Font.cpp index 2116fdd3..74a78932 100644 --- a/src/graphics/core/Font.cpp +++ b/src/graphics/core/Font.cpp @@ -38,11 +38,11 @@ bool Font::isPrintableChar(uint codepoint) const { } } -int Font::calcWidth(const std::wstring& text, size_t length) const { +int Font::calcWidth(std::wstring_view text, size_t length) const { return calcWidth(text, 0, length); } -int Font::calcWidth(const std::wstring& text, size_t offset, size_t length) const { +int Font::calcWidth(std::wstring_view text, size_t offset, size_t length) const { return std::min(text.length()-offset, length) * glyphInterval; } diff --git a/src/graphics/core/Font.hpp b/src/graphics/core/Font.hpp index 07fe0532..eabfbcc6 100644 --- a/src/graphics/core/Font.hpp +++ b/src/graphics/core/Font.hpp @@ -56,14 +56,14 @@ public: /// @param text selected text /// @param length max substring length (default: no limit) /// @return pixel width of the substring - int calcWidth(const std::wstring& text, size_t length=-1) const; + int calcWidth(std::wstring_view text, size_t length=-1) const; /// @brief Calculate text width in pixels /// @param text selected text /// @param offset start of the substring /// @param length max substring length /// @return pixel width of the substring - int calcWidth(const std::wstring& text, size_t offset, size_t length) const; + int calcWidth(std::wstring_view text, size_t offset, size_t length) const; /// @brief Check if character is visible (non-whitespace) /// @param codepoint character unicode codepoint diff --git a/src/graphics/render/TextsRenderer.cpp b/src/graphics/render/TextsRenderer.cpp index 32681ce5..283dacaa 100644 --- a/src/graphics/render/TextsRenderer.cpp +++ b/src/graphics/render/TextsRenderer.cpp @@ -10,6 +10,7 @@ #include "graphics/core/Batch3D.hpp" #include "graphics/core/Shader.hpp" #include "presets/NotePreset.hpp" +#include "constants.hpp" TextsRenderer::TextsRenderer( Batch3D& batch, const Assets& assets, const Frustum& frustum @@ -44,7 +45,7 @@ void TextsRenderer::renderNote( } opacity = preset.xrayOpacity; } - const auto& font = assets.require("normal"); + const auto& font = assets.require(FONT_DEFAULT); glm::vec3 xvec = note.getAxisX(); glm::vec3 yvec = note.getAxisY(); diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 55dd8ceb..95425da4 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -11,14 +11,15 @@ #include "frontend/UiDocument.hpp" #include "frontend/locale.hpp" #include "graphics/core/Batch2D.hpp" +#include "graphics/core/LineBatch.hpp" #include "graphics/core/Shader.hpp" +#include "graphics/core/Font.hpp" #include "graphics/core/DrawContext.hpp" #include "window/Events.hpp" #include "window/Window.hpp" #include "window/input.hpp" #include "window/Camera.hpp" -#include #include #include @@ -34,12 +35,13 @@ GUI::GUI() menu = std::make_shared(); menu->setId("menu"); + menu->setZIndex(10); container->add(menu); container->setScrollable(false); tooltip = guiutil::create( "" - "" + "" "" ); store("tooltip", tooltip); @@ -107,7 +109,7 @@ void GUI::actMouse(float delta) { doubleClicked = false; doubleClickTimer += delta + mouseDelta * 0.1f; - auto hover = container->getAt(Events::cursor, nullptr); + auto hover = container->getAt(Events::cursor); if (this->hover && this->hover != hover) { this->hover->setHover(false); } @@ -238,6 +240,39 @@ void GUI::draw(const DrawContext& pctx, const Assets& assets) { if (hover) { Window::setCursor(hover->getCursor()); } + if (hover && debug) { + auto pos = hover->calcPos(); + const auto& id = hover->getId(); + if (!id.empty()) { + auto& font = assets.require(FONT_DEFAULT); + auto text = util::str2wstr_utf8(id); + int width = font.calcWidth(text); + int height = font.getLineHeight(); + + batch2D->untexture(); + batch2D->setColor(0, 0, 0); + batch2D->rect(pos.x, pos.y, width, height); + + batch2D->resetColor(); + font.draw(*batch2D, text, pos.x, pos.y, nullptr, 0); + } + + batch2D->untexture(); + auto node = hover->getParent(); + while (node) { + auto pos = node->calcPos(); + auto size = node->getSize(); + + batch2D->setColor(0, 255, 255); + batch2D->lineRect(pos.x, pos.y, size.x-1, size.y-1); + + node = node->getParent(); + } + // debug draw + auto size = hover->getSize(); + batch2D->setColor(0, 255, 0); + batch2D->lineRect(pos.x, pos.y, size.x-1, size.y-1); + } } std::shared_ptr GUI::getFocused() const { @@ -252,8 +287,8 @@ void GUI::add(std::shared_ptr node) { container->add(std::move(node)); } -void GUI::remove(std::shared_ptr node) noexcept { - container->remove(std::move(node)); +void GUI::remove(UINode* node) noexcept { + container->remove(node); } void GUI::store(const std::string& name, std::shared_ptr node) { @@ -297,3 +332,7 @@ void GUI::setDoubleClickDelay(float delay) { float GUI::getDoubleClickDelay() const { return doubleClickDelay; } + +void GUI::toggleDebug() { + debug = !debug; +} diff --git a/src/graphics/ui/GUI.hpp b/src/graphics/ui/GUI.hpp index cb6b037e..1099cc89 100644 --- a/src/graphics/ui/GUI.hpp +++ b/src/graphics/ui/GUI.hpp @@ -14,6 +14,7 @@ class DrawContext; class Assets; class Camera; class Batch2D; +class LineBatch; /* Some info about padding and margin. @@ -73,6 +74,7 @@ namespace gui { float doubleClickTimer = 0.0f; float doubleClickDelay = 0.5f; bool doubleClicked = false; + bool debug = false; void actMouse(float delta); void actFocused(); @@ -113,7 +115,11 @@ namespace gui { void add(std::shared_ptr node); /// @brief Remove node from the main container - void remove(std::shared_ptr node) noexcept; + void remove(UINode* node) noexcept; + + void remove(const std::shared_ptr& node) noexcept { + return remove(node.get()); + } /// @brief Store node in the GUI nodes dictionary /// (does not add node to the main container) @@ -144,5 +150,7 @@ namespace gui { void setDoubleClickDelay(float delay); float getDoubleClickDelay() const; + + void toggleDebug(); }; } diff --git a/src/graphics/ui/elements/BasePanel.hpp b/src/graphics/ui/elements/BasePanel.hpp new file mode 100644 index 00000000..6f9bf826 --- /dev/null +++ b/src/graphics/ui/elements/BasePanel.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "Container.hpp" + +namespace gui { + class BasePanel : public Container { + public: + virtual ~BasePanel() = default; + + virtual void setOrientation(Orientation orientation) { + this->orientation = orientation; + refresh(); + } + + Orientation getOrientation() const { + return orientation; + } + + virtual void setPadding(glm::vec4 padding) { + this->padding = padding; + refresh(); + } + + glm::vec4 getPadding() const { + return padding; + } + protected: + BasePanel( + glm::vec2 size, + glm::vec4 padding = glm::vec4(0.0f), + float interval = 2.0f, + Orientation orientation = Orientation::vertical + ) + : Container(std::move(size)), + padding(std::move(padding)), + interval(interval) { + } + + Orientation orientation = Orientation::vertical; + glm::vec4 padding; + float interval = 2.0f; + }; +} diff --git a/src/graphics/ui/elements/Container.cpp b/src/graphics/ui/elements/Container.cpp index 2d4eaf53..3fc25f66 100644 --- a/src/graphics/ui/elements/Container.cpp +++ b/src/graphics/ui/elements/Container.cpp @@ -17,9 +17,7 @@ Container::~Container() { Container::clear(); } -std::shared_ptr Container::getAt( - const glm::vec2& pos, const std::shared_ptr& self -) { +std::shared_ptr Container::getAt(const glm::vec2& pos) { if (!isInteractive() || !isEnabled()) { return nullptr; } @@ -28,19 +26,19 @@ std::shared_ptr Container::getAt( } int diff = (actualLength-size.y); if (scrollable && diff > 0 && pos.x > calcPos().x + getSize().x - scrollBarWidth) { - return UINode::getAt(pos, self); + return UINode::getAt(pos); } for (int i = nodes.size()-1; i >= 0; i--) { auto& node = nodes[i]; if (!node->isVisible()) continue; - auto hover = node->getAt(pos, node); + auto hover = node->getAt(pos); if (hover != nullptr) { return hover; } } - return UINode::getAt(pos, self); + return UINode::getAt(pos); } void Container::mouseMove(GUI* gui, int x, int y) { @@ -172,11 +170,11 @@ void Container::add(const std::shared_ptr& node, glm::vec2 pos) { add(node); } -void Container::remove(const std::shared_ptr& selected) { +void Container::remove(UINode* selected) { selected->setParent(nullptr); nodes.erase(std::remove_if(nodes.begin(), nodes.end(), [selected](const std::shared_ptr& node) { - return node == selected; + return node.get() == selected; } ), nodes.end()); refresh(); @@ -185,7 +183,7 @@ void Container::remove(const std::shared_ptr& selected) { void Container::remove(const std::string& id) { for (auto& node : nodes) { if (node->getId() == id) { - return remove(node); + return remove(node.get()); } } } diff --git a/src/graphics/ui/elements/Container.hpp b/src/graphics/ui/elements/Container.hpp index c51d528a..f91fa915 100644 --- a/src/graphics/ui/elements/Container.hpp +++ b/src/graphics/ui/elements/Container.hpp @@ -28,11 +28,11 @@ namespace gui { virtual void act(float delta) override; virtual void drawBackground(const DrawContext& pctx, const Assets& assets); virtual void draw(const DrawContext& pctx, const Assets& assets) override; - virtual std::shared_ptr getAt(const glm::vec2& pos, const std::shared_ptr& self) override; + virtual std::shared_ptr getAt(const glm::vec2& pos) override; virtual void add(const std::shared_ptr& node); virtual void add(const std::shared_ptr& node, glm::vec2 pos); virtual void clear(); - virtual void remove(const std::shared_ptr& node); + virtual void remove(UINode* node); virtual void remove(const std::string& id); virtual void scrolled(int value) override; virtual void setScrollable(bool flag); diff --git a/src/graphics/ui/elements/InventoryView.cpp b/src/graphics/ui/elements/InventoryView.cpp index a5a2c8a5..099b9c1b 100644 --- a/src/graphics/ui/elements/InventoryView.cpp +++ b/src/graphics/ui/elements/InventoryView.cpp @@ -207,7 +207,7 @@ void SlotView::draw(const DrawContext& pctx, const Assets& assets) { drawItemIcon(batch, stack, item, assets, tint, pos); if (stack.getCount() > 1 || stack.getFields() != nullptr) { - const auto& font = assets.require("normal"); + const auto& font = assets.require(FONT_DEFAULT); drawItemInfo(batch, stack, item, font, pos); } } diff --git a/src/graphics/ui/elements/Label.cpp b/src/graphics/ui/elements/Label.cpp index 392a9434..9a174c74 100644 --- a/src/graphics/ui/elements/Label.cpp +++ b/src/graphics/ui/elements/Label.cpp @@ -35,7 +35,7 @@ uint LabelCache::getLineByTextIndex(size_t index) const { return lines.size()-1; } -void LabelCache::update(const std::wstring& text, bool multiline, bool wrap) { +void LabelCache::update(std::wstring_view text, bool multiline, bool wrap) { resetFlag = false; lines.clear(); lines.push_back(LineScheme {0, false}); @@ -59,6 +59,27 @@ void LabelCache::update(const std::wstring& text, bool multiline, bool wrap) { } } } + if (font != nullptr) { + int lineHeight = font->getLineHeight(); + int maxWidth = 0; + for (int i = 0; i < lines.size() - 1; i++) { + const auto& next = lines[i + 1]; + const auto& cur = lines[i]; + maxWidth = std::max( + font->calcWidth( + text.substr(cur.offset, next.offset - cur.offset) + ), + maxWidth + ); + } + maxWidth = std::max( + font->calcWidth( + text.substr(lines[lines.size() - 1].offset) + ), + maxWidth + ); + multilineWidth = maxWidth; + } } } @@ -89,8 +110,15 @@ glm::vec2 Label::calcSize() { if (cache.lines.size() > 1) { lineHeight *= lineInterval; } + auto view = std::wstring_view(text); + if (multiline) { + return glm::vec2( + cache.multilineWidth, + lineHeight * cache.lines.size() + font->getYOffset() + ); + } return glm::vec2 ( - cache.font->calcWidth(text), + cache.font->calcWidth(view), lineHeight * cache.lines.size() + font->getYOffset() ); } diff --git a/src/graphics/ui/elements/Label.hpp b/src/graphics/ui/elements/Label.hpp index 63637d5f..00d3d0b0 100644 --- a/src/graphics/ui/elements/Label.hpp +++ b/src/graphics/ui/elements/Label.hpp @@ -1,6 +1,7 @@ #pragma once #include "UINode.hpp" +#include "constants.hpp" class Font; struct FontStylesScheme; @@ -17,9 +18,10 @@ namespace gui { /// @brief Reset cache flag bool resetFlag = true; size_t wrapWidth = -1; + int multilineWidth = 0; void prepare(Font* font, size_t wrapWidth); - void update(const std::wstring& text, bool multiline, bool wrap); + void update(std::wstring_view text, bool multiline, bool wrap); size_t getTextLineOffset(size_t line) const; uint getLineByTextIndex(size_t index) const; @@ -61,8 +63,8 @@ namespace gui { std::unique_ptr styles; public: - Label(const std::string& text, std::string fontName="normal"); - Label(const std::wstring& text, std::string fontName="normal"); + Label(const std::string& text, std::string fontName=FONT_DEFAULT); + Label(const std::wstring& text, std::string fontName=FONT_DEFAULT); virtual ~Label(); diff --git a/src/graphics/ui/elements/Menu.cpp b/src/graphics/ui/elements/Menu.cpp index d059d693..d18f725a 100644 --- a/src/graphics/ui/elements/Menu.cpp +++ b/src/graphics/ui/elements/Menu.cpp @@ -55,7 +55,7 @@ void Menu::setPage(const std::string &name, bool history) { void Menu::setPage(Page page, bool history) { if (current.panel) { - Container::remove(current.panel); + Container::remove(current.panel.get()); if (history && !current.temporal) { pageStack.push(current); } @@ -104,7 +104,7 @@ void Menu::clearHistory() { void Menu::reset() { clearHistory(); if (current.panel) { - Container::remove(current.panel); + Container::remove(current.panel.get()); current = Page {"", nullptr}; } } diff --git a/src/graphics/ui/elements/Panel.cpp b/src/graphics/ui/elements/Panel.cpp index 70446d7d..4cc3730e 100644 --- a/src/graphics/ui/elements/Panel.cpp +++ b/src/graphics/ui/elements/Panel.cpp @@ -5,9 +5,7 @@ using namespace gui; Panel::Panel(glm::vec2 size, glm::vec4 padding, float interval) - : Container(size), - padding(padding), - interval(interval) + : BasePanel(size, padding, interval, Orientation::vertical) { setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.75f)); } @@ -31,15 +29,6 @@ int Panel::getMinLength() const { return minLength; } -void Panel::setPadding(glm::vec4 padding) { - this->padding = padding; - refresh(); -} - -glm::vec4 Panel::getPadding() const { - return padding; -} - void Panel::cropToContent() { if (maxLength > 0.0f) { setSize(glm::vec2( @@ -63,7 +52,7 @@ void Panel::add(const std::shared_ptr &node) { fullRefresh(); } -void Panel::remove(const std::shared_ptr &node) { +void Panel::remove(UINode* node) { Container::remove(node); fullRefresh(); } @@ -109,11 +98,3 @@ void Panel::refresh() { actualLength = size.y; } } - -void Panel::setOrientation(Orientation orientation) { - this->orientation = orientation; -} - -Orientation Panel::getOrientation() const { - return orientation; -} diff --git a/src/graphics/ui/elements/Panel.hpp b/src/graphics/ui/elements/Panel.hpp index 5a6399ce..7a16c018 100644 --- a/src/graphics/ui/elements/Panel.hpp +++ b/src/graphics/ui/elements/Panel.hpp @@ -1,31 +1,22 @@ #pragma once #include "commons.hpp" -#include "Container.hpp" +#include "BasePanel.hpp" namespace gui { - class Panel : public Container { - protected: - Orientation orientation = Orientation::vertical; - glm::vec4 padding {2.0f}; - float interval = 2.0f; - int minLength = 0; - int maxLength = 0; + class Panel : public BasePanel { public: Panel( glm::vec2 size, - glm::vec4 padding=glm::vec4(2.0f), + glm::vec4 padding=glm::vec4(0.0f), float interval=2.0f ); virtual ~Panel(); virtual void cropToContent(); - virtual void setOrientation(Orientation orientation); - Orientation getOrientation() const; - virtual void add(const std::shared_ptr& node) override; - virtual void remove(const std::shared_ptr& node) override; + virtual void remove(UINode* node) override; virtual void refresh() override; virtual void fullRefresh() override; @@ -35,8 +26,8 @@ namespace gui { virtual void setMinLength(int value); int getMinLength() const; - - virtual void setPadding(glm::vec4 padding); - glm::vec4 getPadding() const; + protected: + int minLength = 0; + int maxLength = 0; }; } diff --git a/src/graphics/ui/elements/Plotter.cpp b/src/graphics/ui/elements/Plotter.cpp index 6cf484ea..860f6b69 100644 --- a/src/graphics/ui/elements/Plotter.cpp +++ b/src/graphics/ui/elements/Plotter.cpp @@ -5,6 +5,7 @@ #include "graphics/core/DrawContext.hpp" #include "assets/Assets.hpp" #include "util/stringutil.hpp" +#include "constants.hpp" using namespace gui; @@ -37,7 +38,7 @@ void Plotter::draw(const DrawContext& pctx, const Assets& assets) { } int current_point = static_cast(points[index % dmwidth]); - auto font = assets.get("normal"); + auto font = assets.get(FONT_DEFAULT); for (int y = 0; y < dmheight; y += labelsInterval) { std::wstring string; if (current_point/16 == y/labelsInterval) { diff --git a/src/graphics/ui/elements/Plotter.hpp b/src/graphics/ui/elements/Plotter.hpp index 776dda31..13fa3d89 100644 --- a/src/graphics/ui/elements/Plotter.hpp +++ b/src/graphics/ui/elements/Plotter.hpp @@ -4,7 +4,6 @@ #include "typedefs.hpp" #include -#include class Assets; class DrawContext; diff --git a/src/graphics/ui/elements/SplitBox.cpp b/src/graphics/ui/elements/SplitBox.cpp new file mode 100644 index 00000000..8729835e --- /dev/null +++ b/src/graphics/ui/elements/SplitBox.cpp @@ -0,0 +1,74 @@ +#include "SplitBox.hpp" + +using namespace gui; + +SplitBox::SplitBox(const glm::vec2& size, float splitPos, Orientation orientation) + : BasePanel(size, glm::vec4(), 4.0f, orientation), splitPos(splitPos) { + setCursor( + orientation == Orientation::vertical ? CursorShape::NS_RESIZE + : CursorShape::EW_RESIZE + ); +} + +void SplitBox::mouseMove(GUI*, int x, int y) { + auto pos = calcPos(); + auto size = getSize(); + + glm::ivec2 cursor(x - pos.x, y - pos.y); + int axis = orientation == Orientation::vertical; + + int v = cursor[axis]; + v = std::max(std::min(static_cast(size[axis]) - 10, v), 10); + float t = v / size[axis]; + splitPos = t; + + refresh(); +} + +void SplitBox::refresh() { + Container::refresh(); + + if (nodes.empty()) { + return; + } + glm::vec2 size = getSize(); + if (nodes.size() == 1) { + auto node = nodes.at(0); + node->setPos(glm::vec2()); + node->setSize(size); + return; + } + auto nodeA = nodes.at(0); + auto nodeB = nodes.at(1); + + float sepRadius = interval / 2.0f; + + nodeA->setPos(glm::vec2(padding)); + + const auto& p = padding; + if (orientation == Orientation::vertical) { + float splitPos = this->splitPos * size.y; + nodeA->setSize({size.x-p.x-p.z, splitPos - sepRadius - p.y}); + nodeB->setSize({size.x-p.x-p.z, size.y - splitPos - sepRadius - p.w}); + nodeB->setPos({p.x, splitPos + sepRadius}); + } else { + float splitPos = this->splitPos * size.x; + nodeA->setSize({splitPos - sepRadius - p.x, size.y - p.y - p.w}); + nodeB->setSize({size.x - splitPos - sepRadius - p.z, size.y - p.y - p.w}); + nodeB->setPos({splitPos + sepRadius, p.y}); + } +} + +void SplitBox::doubleClick(GUI*, int x, int y) { + if (nodes.size() < 2) { + return; + } + std::swap(nodes[0], nodes[1]); + refresh(); +} + +void SplitBox::fullRefresh() { + refresh(); + reposition(); + Container::fullRefresh(); +} diff --git a/src/graphics/ui/elements/SplitBox.hpp b/src/graphics/ui/elements/SplitBox.hpp new file mode 100644 index 00000000..7e7ca88b --- /dev/null +++ b/src/graphics/ui/elements/SplitBox.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "BasePanel.hpp" + +namespace gui { + class SplitBox : public BasePanel { + public: + SplitBox(const glm::vec2& size, float splitPos, Orientation orientation); + + virtual void mouseMove(GUI*, int x, int y) override; + virtual void refresh() override; + virtual void fullRefresh() override; + virtual void doubleClick(GUI*, int x, int y) override; + private: + float splitPos; + }; +} diff --git a/src/graphics/ui/elements/TextBox.cpp b/src/graphics/ui/elements/TextBox.cpp index 791dcde8..96d9ea01 100644 --- a/src/graphics/ui/elements/TextBox.cpp +++ b/src/graphics/ui/elements/TextBox.cpp @@ -13,18 +13,185 @@ #include "util/stringutil.hpp" #include "window/Events.hpp" #include "window/Window.hpp" +#include "devtools/actions.hpp" #include "../markdown.hpp" using namespace gui; inline constexpr int LINE_NUMBERS_PANE_WIDTH = 40; -TextBox::TextBox(std::wstring placeholder, glm::vec4 padding) - : Container(glm::vec2(200,32)), - padding(padding), - input(L""), - placeholder(std::move(placeholder)) -{ +class InputAction : public Action { + std::weak_ptr textbox; + size_t position; + std::wstring string; +public: + InputAction( + std::weak_ptr textbox, size_t position, std::wstring string + ) + : textbox(std::move(textbox)), + position(position), + string(std::move(string)) { + } + void apply() override { + if (auto box = textbox.lock()) { + box->select(position, position); + box->paste(string); + } + } + + void revert() override { + if (auto box = textbox.lock()) { + box->select(position, position); + box->erase(position, string.length()); + } + } +}; + +class SelectionAction : public Action { + std::weak_ptr textbox; + size_t start; + size_t end; +public: + SelectionAction(std::weak_ptr textbox, size_t start, size_t end) + : textbox(std::move(textbox)), start(start), end(end) {} + + void apply() override { + if (auto box = textbox.lock()) { + box->select(start, end); + } + } + + void revert() override { + if (auto box = textbox.lock()) { + box->select(0, 0); + } + } +}; + +namespace gui { + /// @brief Accumulates small changes into words for InputAction creation + class TextBoxHistorian { + public: + TextBoxHistorian(TextBox& textBox, ActionsHistory& history) + : textBox(textBox), history(history) { + } + + void onPaste(size_t pos, std::wstring_view text) { + if (locked) { + return; + } + if (erasing) { + sync(); + } + if (this->pos == static_cast(-1)) { + this->pos = pos; + } + if (this->pos + length != pos || text == L" " || text == L"\n") { + sync(); + this->pos = pos; + } + ss << text; + length += text.length(); + } + + void onErase(size_t pos, std::wstring_view text, bool selection=false) { + if (locked) { + return; + } + if (!erasing) { + sync(); + erasing = true; + } + if (selection) { + history.store( + std::make_unique( + getTextBoxWeakptr(), + textBox.getSelectionStart(), + textBox.getSelectionEnd() + ), + true + ); + } + if (this->pos == static_cast(-1)) { + this->pos = pos; + } else if (this->pos - text.length() != pos) { + sync(); + erasing = true; + this->pos = pos; + } + if (text == L" " || text == L"\n") { + sync(); + erasing = true; + this->pos = pos; + } + auto str = ss.str(); + ss.seekp(0); + ss << text << str; + + this->pos = pos; + length += text.length(); + } + + /// @brief Flush buffer and push all changes to the ActionsHistory + void sync() { + auto string = ss.str(); + if (string.empty()) { + return; + } + auto action = + std::make_unique(getTextBoxWeakptr(), pos, string); + history.store(std::move(action), erasing); + reset(); + } + + void undo() { + sync(); + locked = true; + history.undo(); + locked = false; + } + + void redo() { + sync(); + locked = true; + history.redo(); + locked = false; + } + + void reset() { + pos = -1; + length = 0; + erasing = false; + ss = {}; + } + + bool isSynced() const { + return length == 0; + } + private: + TextBox& textBox; + ActionsHistory& history; + std::wstringstream ss; + size_t pos = -1; + size_t length = 0; + bool erasing = false; + bool locked = false; + + std::weak_ptr getTextBoxWeakptr() { + return std::weak_ptr(std::dynamic_pointer_cast( + textBox.shared_from_this() + )); + } + }; +} + +TextBox::TextBox(std::wstring placeholder, glm::vec4 padding) + : Container(glm::vec2(200, 32)), + history(std::make_shared()), + historian(std::make_unique(*this, *history)), + padding(padding), + input(L""), + placeholder(std::move(placeholder)) { setCursor(CursorShape::TEXT); setOnUpPressed(nullptr); setOnDownPressed(nullptr); @@ -49,6 +216,8 @@ TextBox::TextBox(std::wstring placeholder, glm::vec4 padding) scrollStep = 0; } +TextBox::~TextBox() = default; + void TextBox::draw(const DrawContext& pctx, const Assets& assets) { Container::draw(pctx, assets); @@ -71,6 +240,7 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) { auto batch = pctx.getBatch2D(); batch->texture(nullptr); batch->setColor(glm::vec4(1.0f)); + if (editable && int((Window::time() - caretLastMove) * 2) % 2 == 0) { uint line = rawTextCache.getLineByTextIndex(caret); uint lcaret = caret - rawTextCache.getTextLineOffset(line); @@ -138,7 +308,6 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) { } do { int lineY = label->getLineYOffset(line); - int lineHeight = font->getLineHeight() * label->getLineInterval(); batch->setColor(glm::vec4(1, 1, 1, 0.05f)); if (showLineNumbers) { @@ -260,18 +429,22 @@ void TextBox::refreshLabel() { /// @brief Insert text at the caret. Also selected text will be erased /// @param text Inserting text -void TextBox::paste(const std::wstring& text) { +void TextBox::paste(const std::wstring& text, bool history) { eraseSelected(); + auto inputText = text; + inputText.erase( + std::remove(inputText.begin(), inputText.end(), '\r'), inputText.end() + ); + historian->onPaste(caret, inputText); if (caret >= input.length()) { - input += text; + input += inputText; } else { auto left = input.substr(0, caret); auto right = input.substr(caret); - input = left + text + right; + input = left + inputText + right; } - input.erase(std::remove(input.begin(), input.end(), '\r'), input.end()); refreshLabel(); - setCaret(caret + text.length()); + setCaret(caret + inputText.length()); if (validate()) { onInput(); } @@ -296,6 +469,11 @@ bool TextBox::eraseSelected() { if (selectionStart == selectionEnd) { return false; } + historian->onErase( + selectionStart, + input.substr(selectionStart, selectionEnd - selectionStart), + true + ); erase(selectionStart, selectionEnd-selectionStart); resetSelection(); onInput(); @@ -336,7 +514,9 @@ void TextBox::setTextOffset(uint x) { void TextBox::typed(unsigned int codepoint) { if (editable) { - paste(std::wstring({(wchar_t)codepoint})); + // Combine deleting selected text and inserting a symbol + auto combination = history->beginCombination(); + paste(std::wstring({static_cast(codepoint)})); } } @@ -383,6 +563,23 @@ bool TextBox::isEditable() const { return editable; } +bool TextBox::isEdited() const { + return history->size() != editedHistorySize || !historian->isSynced(); +} + +void TextBox::setUnedited() { + historian->sync(); + editedHistorySize = history->size(); +} + +size_t TextBox::getSelectionStart() const { + return selectionStart; +} + +size_t TextBox::getSelectionEnd() const { + return selectionEnd; +} + void TextBox::setOnEditStart(runnable oneditstart) { onEditStart = oneditstart; } @@ -404,6 +601,12 @@ void TextBox::onFocus(GUI* gui) { } } +void TextBox::reposition() { + auto size = getSize(); + UINode::reposition(); + refreshLabel(); +} + void TextBox::refresh() { Container::refresh(); label->setSize(size-glm::vec2(padding.z+padding.x, padding.w+padding.y)); @@ -609,6 +812,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) { if (caret > input.length()) { caret = input.length(); } + historian->onErase(caret - 1, input.substr(caret - 1, 1)); input = input.substr(0, caret-1) + input.substr(caret); setCaret(caret-1); if (validate()) { @@ -617,6 +821,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) { } } else if (key == keycode::DELETE) { if (!eraseSelected() && caret < input.length()) { + historian->onErase(caret, input.substr(caret, 1)); input = input.substr(0, caret) + input.substr(caret + 1); if (validate()) { onInput(); @@ -648,7 +853,12 @@ void TextBox::keyPressed(keycode key) { if (editable) { performEditingKeyboardEvents(key); } - if (Events::pressed(keycode::LEFT_CONTROL)) { + if (Events::pressed(keycode::LEFT_CONTROL) && key != keycode::LEFT_CONTROL) { + if (controlCombinationsHandler) { + if (controlCombinationsHandler(static_cast(key))) { + return; + } + } // Copy selected text to clipboard if (key == keycode::C || key == keycode::X) { std::string text = util::wstr2str_utf8(getSelection()); @@ -663,7 +873,11 @@ void TextBox::keyPressed(keycode key) { if (key == keycode::V && editable) { const char* text = Window::getClipboardText(); if (text) { + historian->sync(); // flush buffer before combination + // Combine deleting selected text and pasing a clipboard content + auto combination = history->beginCombination(); paste(util::str2wstr_utf8(text)); + historian->sync(); } } // Select/deselect all @@ -674,6 +888,14 @@ void TextBox::keyPressed(keycode key) { resetSelection(); } } + if (key == keycode::Z) { + historian->undo(); + refreshSyntax(); + } + if (key == keycode::Y) { + historian->redo(); + refreshSyntax(); + } } } @@ -698,10 +920,8 @@ size_t TextBox::getLinePos(uint line) const { return label->getTextLineOffset(line); } -std::shared_ptr TextBox::getAt( - const glm::vec2& pos, const std::shared_ptr& self -) { - return UINode::getAt(pos, self); +std::shared_ptr TextBox::getAt(const glm::vec2& pos) { + return UINode::getAt(pos); } void TextBox::setOnUpPressed(const runnable &callback) { @@ -752,6 +972,10 @@ void TextBox::setTextValidator(wstringchecker validator) { this->validator = std::move(validator); } +void TextBox::setOnControlCombination(key_handler handler) { + this->controlCombinationsHandler = std::move(handler); +} + void TextBox::setFocusedColor(glm::vec4 color) { this->focusedColor = color; } @@ -786,6 +1010,9 @@ const std::wstring& TextBox::getText() const { void TextBox::setText(const std::wstring& value) { this->input = value; input.erase(std::remove(input.begin(), input.end(), '\r'), input.end()); + historian->reset(); + history->clear(); + editedHistorySize = 0; refreshSyntax(); } diff --git a/src/graphics/ui/elements/TextBox.hpp b/src/graphics/ui/elements/TextBox.hpp index 0eff89f2..7fddfacb 100644 --- a/src/graphics/ui/elements/TextBox.hpp +++ b/src/graphics/ui/elements/TextBox.hpp @@ -4,10 +4,15 @@ #include "Label.hpp" class Font; +class ActionsHistory; namespace gui { + class TextBoxHistorian; class TextBox : public Container { LabelCache rawTextCache; + std::shared_ptr history; + std::unique_ptr historian; + int editedHistorySize = 0; protected: glm::vec4 focusedColor {0.0f, 0.0f, 0.0f, 1.0f}; glm::vec4 invalidColor {0.1f, 0.05f, 0.03f, 1.0f}; @@ -29,6 +34,7 @@ namespace gui { wstringconsumer subconsumer = nullptr; /// @brief Text validator returning boolean value wstringchecker validator = nullptr; + key_handler controlCombinationsHandler = nullptr; /// @brief Function called on focus runnable onEditStart = nullptr; /// @brief Function called on up arrow pressed @@ -68,7 +74,6 @@ namespace gui { int calcIndexAt(int x, int y) const; void setTextOffset(uint x); - void erase(size_t start, size_t length); bool eraseSelected(); void resetSelection(); void extendSelection(int index); @@ -93,8 +98,11 @@ namespace gui { std::wstring placeholder, glm::vec4 padding=glm::vec4(4.0f) ); + + virtual ~TextBox(); - void paste(const std::wstring& text); + void paste(const std::wstring& text, bool history=true); + void erase(size_t start, size_t length); virtual void setTextSupplier(wstringsupplier supplier); @@ -111,6 +119,8 @@ namespace gui { /// @param validator std::wstring consumer returning boolean virtual void setTextValidator(wstringchecker validator); + virtual void setOnControlCombination(key_handler handler); + virtual void setFocusedColor(glm::vec4 color); virtual glm::vec4 getFocusedColor() const; @@ -198,9 +208,15 @@ namespace gui { /// @brief Check if text editing feature is enabled virtual bool isEditable() const; + virtual bool isEdited() const; + virtual void setUnedited(); + virtual void setPadding(glm::vec4 padding); glm::vec4 getPadding() const; + size_t getSelectionStart() const; + size_t getSelectionEnd() const; + /// @brief Set runnable called on textbox focus virtual void setOnEditStart(runnable oneditstart); @@ -210,6 +226,7 @@ namespace gui { virtual void setShowLineNumbers(bool flag); virtual bool isShowLineNumbers() const; + virtual void reposition() override; virtual void onFocus(GUI*) override; virtual void refresh() override; virtual void doubleClick(GUI*, int x, int y) override; @@ -220,9 +237,7 @@ namespace gui { virtual void drawBackground(const DrawContext& pctx, const Assets& assets) override; virtual void typed(unsigned int codepoint) override; virtual void keyPressed(keycode key) override; - virtual std::shared_ptr getAt( - const glm::vec2& pos, const std::shared_ptr& self - ) override; + virtual std::shared_ptr getAt(const glm::vec2& pos) override; virtual void setOnUpPressed(const runnable &callback); virtual void setOnDownPressed(const runnable &callback); diff --git a/src/graphics/ui/elements/UINode.cpp b/src/graphics/ui/elements/UINode.cpp index f6444f41..d975315a 100644 --- a/src/graphics/ui/elements/UINode.cpp +++ b/src/graphics/ui/elements/UINode.cpp @@ -111,11 +111,11 @@ bool UINode::isInside(glm::vec2 point) { point.x < pos.x + size.x && point.y < pos.y + size.y); } -std::shared_ptr UINode::getAt(const glm::vec2& point, const std::shared_ptr& self) { +std::shared_ptr UINode::getAt(const glm::vec2& point) { if (!isInteractive() || !enabled) { return nullptr; } - return isInside(point) ? self : nullptr; + return isInside(point) ? shared_from_this() : nullptr; } bool UINode::isInteractive() const { @@ -266,7 +266,7 @@ void UINode::moveInto( ) { auto parent = node->getParent(); if (auto container = dynamic_cast(parent)) { - container->remove(node); + container->remove(node.get()); } if (parent) { parent->scrolled(0); @@ -301,9 +301,13 @@ const std::string& UINode::getId() const { void UINode::reposition() { if (sizefunc) { auto newSize = sizefunc(); + auto defsize = newSize; + if (parent) { + defsize = parent->getSize(); + } setSize( - {newSize.x < 0 ? size.x : newSize.x, - newSize.y < 0 ? size.y : newSize.y} + {newSize.x < 0 ? defsize.x + (newSize.x + 1) : newSize.x, + newSize.y < 0 ? defsize.y + (newSize.y + 1) : newSize.y} ); } if (positionfunc) { diff --git a/src/graphics/ui/elements/UINode.hpp b/src/graphics/ui/elements/UINode.hpp index 2f62dd27..beb1bf20 100644 --- a/src/graphics/ui/elements/UINode.hpp +++ b/src/graphics/ui/elements/UINode.hpp @@ -63,7 +63,7 @@ namespace gui { }; /// @brief Base abstract class for all UI elements - class UINode { + class UINode : public std::enable_shared_from_this { /// @brief element identifier used for direct access in UiDocument std::string id = ""; /// @brief element enabled state @@ -195,7 +195,7 @@ namespace gui { /// @param pos cursor screen position /// @param self shared pointer to element /// @return self, sub-element or nullptr if element is not interractive - virtual std::shared_ptr getAt(const glm::vec2& pos, const std::shared_ptr& self); + virtual std::shared_ptr getAt(const glm::vec2& pos); /// @brief Check if element is opaque for cursor virtual bool isInteractive() const; @@ -250,7 +250,7 @@ namespace gui { const std::string& getId() const; /// @brief Fetch pos from positionfunc if assigned - void reposition(); + virtual void reposition(); virtual void setGravity(Gravity gravity); diff --git a/src/graphics/ui/gui_util.cpp b/src/graphics/ui/gui_util.cpp index 7fd40431..30b12390 100644 --- a/src/graphics/ui/gui_util.cpp +++ b/src/graphics/ui/gui_util.cpp @@ -91,7 +91,11 @@ void guiutil::confirm( if (yestext.empty()) yestext = langs::get(L"Yes"); if (notext.empty()) notext = langs::get(L"No"); + auto container = std::make_shared(glm::vec2(5000, 5000)); + container->setColor(glm::vec4(0.05f, 0.05f, 0.05f, 0.7f)); auto panel = std::make_shared(glm::vec2(600, 200), glm::vec4(8.0f), 8.0f); + panel->setGravity(Gravity::center_center); + container->add(panel); panel->setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.5f)); panel->add(std::make_shared