From 256808630b7ad45e56bacd70b095a0953e869d9d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 26 Feb 2024 15:12:02 +0300 Subject: [PATCH] multiline textbox mode --- src/frontend/gui/containers.cpp | 5 +- src/frontend/gui/containers.h | 1 + src/frontend/gui/controls.cpp | 211 ++++++++++++++++++++++++++++---- src/frontend/gui/controls.h | 41 ++++++- src/frontend/hud.cpp | 2 +- src/util/timeutil.cpp | 4 - src/util/timeutil.h | 19 +-- src/voxels/ChunksStorage.cpp | 2 +- src/world/Level.cpp | 2 +- src/world/World.cpp | 21 ++-- src/world/World.h | 3 +- 11 files changed, 260 insertions(+), 51 deletions(-) diff --git a/src/frontend/gui/containers.cpp b/src/frontend/gui/containers.cpp index 10cb0468..e743f5af 100644 --- a/src/frontend/gui/containers.cpp +++ b/src/frontend/gui/containers.cpp @@ -60,8 +60,11 @@ void Container::act(float delta) { void Container::scrolled(int value) { int diff = (actualLength-getSize().y); + if (scroll < 0 && diff <= 0) { + scroll = 0; + } if (diff > 0 && scrollable) { - scroll += value * 40; + scroll += value * scrollStep; if (scroll > 0) scroll = 0; if (-scroll > diff) { diff --git a/src/frontend/gui/containers.h b/src/frontend/gui/containers.h index 6ebe21b4..cc8cc414 100644 --- a/src/frontend/gui/containers.h +++ b/src/frontend/gui/containers.h @@ -29,6 +29,7 @@ namespace gui { std::vector> nodes; std::vector intervalEvents; int scroll = 0; + int scrollStep = 40; int actualLength = 0; bool scrollable = true; public: diff --git a/src/frontend/gui/controls.cpp b/src/frontend/gui/controls.cpp index 3a5a35db..cc058f19 100644 --- a/src/frontend/gui/controls.cpp +++ b/src/frontend/gui/controls.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "../../window/Events.h" #include "../../assets/Assets.h" @@ -73,10 +74,52 @@ int Label::getTextYOffset() const { return textYOffset; } +size_t Label::getTextLineOffset(uint line) const { + size_t offset = 0; + size_t linesCount = 0; + while (linesCount < line && offset < text.length()) { + size_t endline = text.find(L'\n', offset); + if (endline == std::wstring::npos) { + break; + } + offset = endline+1; + linesCount++; + } + return offset; +} + int Label::getLineYOffset(uint line) const { return line * totalLineHeight + textYOffset; } +uint Label::getLineByYOffset(int offset) const { + if (offset < textYOffset) { + return 0; + } + return (offset - textYOffset) / totalLineHeight; +} + +uint Label::getLineByTextIndex(size_t index) const { + size_t offset = 0; + size_t linesCount = 0; + while (offset < index && offset < text.length()) { + size_t endline = text.find(L'\n', offset); + if (endline == std::wstring::npos) { + break; + } + if (endline+1 > index) { + break; + } + offset = endline+1; + linesCount++; + } + return linesCount; +} + +uint Label::getLinesNumber() const { + return lines; +} + void Label::draw(const GfxContext* pctx, Assets* assets) { if (supplier) { setText(supplier()); @@ -115,7 +158,7 @@ void Label::draw(const GfxContext* pctx, Assets* assets) { coord.y += size.y-newsize.y; break; } - textYOffset = coord.y; + textYOffset = coord.y-calcCoord().y; totalLineHeight = lineHeight * lineInterval; if (multiline) { @@ -334,23 +377,46 @@ void TextBox::draw(const GfxContext* pctx, Assets* assets) { if (!isFocused()) return; - const int yoffset = 0; - const int lineHeight = font->getLineHeight(); + glm::vec2 coord = calcCoord(); + glm::vec2 size = getSize(); + + auto subctx = pctx->sub(); + subctx.scissors(glm::vec4(coord.x, coord.y, size.x, size.y)); + + const int lineHeight = font->getLineHeight() * label->getLineInterval(); glm::vec2 lcoord = label->calcCoord(); + lcoord.y -= 2; auto batch = pctx->getBatch2D(); batch->texture(nullptr); if (int((Window::time() - caretLastMove) * 2) % 2 == 0) { + uint line = label->getLineByTextIndex(caret); + uint lcaret = caret - label->getTextLineOffset(line); batch->setColor(glm::vec4(1.0f)); - int width = font->calcWidth(input, caret); - batch->rect(lcoord.x + width, lcoord.y+yoffset, 2, lineHeight); + int width = font->calcWidth(input, lcaret); + batch->rect(lcoord.x + width, lcoord.y+label->getLineYOffset(line), 2, lineHeight); } if (selectionStart != selectionEnd) { - batch->setColor(glm::vec4(0.8f, 0.9f, 1.0f, 0.5f)); - int start = font->calcWidth(input, selectionStart); - int end = font->calcWidth(input, selectionEnd); - batch->rect(lcoord.x + start, lcoord.y+yoffset, end-start, lineHeight); + uint startLine = label->getLineByTextIndex(selectionStart); + uint endLine = label->getLineByTextIndex(selectionEnd); + + batch->setColor(glm::vec4(0.8f, 0.9f, 1.0f, 0.25f)); + int start = font->calcWidth(input, selectionStart-label->getTextLineOffset(startLine)); + int end = font->calcWidth(input, selectionEnd-label->getTextLineOffset(endLine)); + int startY = label->getLineYOffset(startLine); + int endY = label->getLineYOffset(startLine); + + if (startLine == endLine) { + batch->rect(lcoord.x + start, lcoord.y+startY, end-start, lineHeight); + } else { + batch->rect(lcoord.x + start, lcoord.y+endY, label->getSize().x-start-padding.z-padding.x-2, lineHeight); + for (uint i = startLine+1; i < endLine; i++) { + batch->rect(lcoord.x, lcoord.y+label->getLineYOffset(i), label->getSize().x-padding.z-padding.x-2, lineHeight); + } + batch->rect(lcoord.x, lcoord.y+label->getLineYOffset(endLine), end, lineHeight); + } } + batch->flush(); } void TextBox::drawBackground(const GfxContext* pctx, Assets* assets) { @@ -362,7 +428,7 @@ void TextBox::drawBackground(const GfxContext* pctx, Assets* assets) { if (valid) { if (isFocused()) { batch->setColor(focusedColor); - } else if (hover) { + } else if (hover && !multiline) { batch->setColor(hoverColor); } else { batch->setColor(color); @@ -378,12 +444,20 @@ void TextBox::drawBackground(const GfxContext* pctx, Assets* assets) { label->setColor(glm::vec4(input.empty() ? 0.5f : 1.0f)); label->setText(getText()); - setScrollable(false); + if (multiline && font) { + setScrollable(true); + uint height = label->getLinesNumber() * font->getLineHeight() * label->getLineInterval(); + label->setSize(glm::vec2(label->getSize().x, height)); + actualLength = height; + } else { + setScrollable(false); + } } /// @brief Insert text at the caret. Also selected text will be erased /// @param text Inserting text void TextBox::paste(const std::wstring& text) { + eraseSelected(); if (caret >= input.length()) { input += text; @@ -391,7 +465,11 @@ void TextBox::paste(const std::wstring& text) { auto left = input.substr(0, caret); auto right = input.substr(caret); input = left + text + right; + input.erase(std::remove(input.begin(), input.end(), '\r'), input.end()); } + // refresh label lines configuration for correct setCaret work + label->setText(input); + setCaret(caret + text.length()); validate(); } @@ -432,6 +510,18 @@ void TextBox::extendSelection(int index) { selectionEnd = std::max(selectionOrigin, normalized); } +size_t TextBox::getLineLength(uint line) const { + size_t position = label->getTextLineOffset(line); + size_t lineLength = label->getTextLineOffset(line+1)-position; + if (lineLength == 0) + lineLength = input.length() - position + 1; + return lineLength; +} + +size_t TextBox::getSelectionLength() const { + return selectionEnd - selectionStart; +} + /// @brief Set scroll offset /// @param x scroll offset void TextBox::setTextOffset(uint x) { @@ -496,33 +586,45 @@ size_t TextBox::normalizeIndex(int index) { /// @brief Calculate index of character at defined screen X position /// @param x screen X position +/// @param y screen Y position /// @return non-normalized character index -int TextBox::calcIndexAt(int x) const { +int TextBox::calcIndexAt(int x, int y) const { if (font == nullptr) return 0; glm::vec2 lcoord = label->calcCoord(); + uint line = label->getLineByYOffset(y-lcoord.y); + line = std::min(line, label->getLinesNumber()-1); + size_t lineLength = getLineLength(line); uint offset = 0; - while (lcoord.x + font->calcWidth(input, offset) < x && offset <= input.length()) { + while (lcoord.x + font->calcWidth(input, offset) < x && offset < lineLength-1) { offset++; } - return offset; + return std::min(offset+label->getTextLineOffset(line), input.length()); } -void TextBox::click(GUI*, int x, int) { - int index = normalizeIndex(calcIndexAt(x)); +void TextBox::click(GUI*, int x, int y) { + int index = normalizeIndex(calcIndexAt(x, y)); selectionStart = index; selectionEnd = index; selectionOrigin = index; } void TextBox::mouseMove(GUI*, int x, int y) { - int index = calcIndexAt(x); + int index = calcIndexAt(x, y); setCaret(index); extendSelection(index); + resetMaxLocalCaret(); } +void TextBox::resetMaxLocalCaret() { + maxLocalCaret = caret - label->getTextLineOffset(label->getLineByTextIndex(caret)); +} + + +// TODO: refactor void TextBox::keyPressed(keycode key) { bool shiftPressed = Events::pressed(keycode::LEFT_SHIFT); + bool breakSelection = getSelectionLength() != 0 && !shiftPressed; uint previousCaret = caret; if (key == keycode::BACKSPACE) { if (!eraseSelected() && caret > 0 && input.length() > 0) { @@ -539,11 +641,18 @@ void TextBox::keyPressed(keycode key) { validate(); } } else if (key == keycode::ENTER) { - if (validate() && consumer) { - consumer(label->getText()); + if (multiline) { + paste(L"\n"); + } else { + if (validate() && consumer) { + consumer(label->getText()); + } + defocus(); } - defocus(); + } else if (key == keycode::TAB) { + paste(L" "); } else if (key == keycode::LEFT) { + uint caret = breakSelection ? selectionStart : this->caret; if (caret > 0) { if (caret > input.length()) { setCaret(input.length()-1); @@ -554,12 +663,17 @@ void TextBox::keyPressed(keycode key) { if (selectionStart == selectionEnd) { selectionOrigin = previousCaret; } - extendSelection(caret); + extendSelection(this->caret); } else { resetSelection(); } + } else { + setCaret(caret); + resetSelection(); } + resetMaxLocalCaret(); } else if (key == keycode::RIGHT) { + uint caret = breakSelection ? selectionEnd : this->caret; if (caret < input.length()) { setCaret(caret+1); caretLastMove = Window::time(); @@ -567,10 +681,48 @@ void TextBox::keyPressed(keycode key) { if (selectionStart == selectionEnd) { selectionOrigin = previousCaret; } - extendSelection(caret); + extendSelection(this->caret); } else { resetSelection(); } + } else { + setCaret(caret); + resetSelection(); + } + resetMaxLocalCaret(); + } else if (key == keycode::UP) { + uint caret = breakSelection ? selectionStart : this->caret; + uint caretLine = label->getLineByTextIndex(caret); + if (caretLine > 0) { + uint offset = std::min(size_t(maxLocalCaret), getLineLength(caretLine-1)-1); + setCaret(label->getTextLineOffset(caretLine-1) + offset); + } else { + setCaret(0); + } + if (shiftPressed) { + if (selectionStart == selectionEnd) { + selectionOrigin = previousCaret; + } + extendSelection(this->caret); + } else { + resetSelection(); + } + } else if (key == keycode::DOWN) { + uint caret = breakSelection ? selectionEnd : this->caret; + uint caretLine = label->getLineByTextIndex(caret); + if (caretLine < label->getLinesNumber()-1) { + uint offset = std::min(size_t(maxLocalCaret), getLineLength(caretLine+1)-1); + setCaret(label->getTextLineOffset(caretLine+1) + offset); + } else { + setCaret(input.length()); + } + if (shiftPressed) { + if (selectionStart == selectionEnd) { + selectionOrigin = previousCaret; + } + extendSelection(this->caret); + } else { + resetSelection(); } } if (Events::pressed(keycode::LEFT_CONTROL)) { @@ -655,6 +807,7 @@ 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()); } std::wstring TextBox::getPlaceholder() const { @@ -674,11 +827,21 @@ uint TextBox::getCaret() const { } void TextBox::setCaret(uint position) { - this->caret = position; + this->caret = std::min(size_t(position), input.length()); caretLastMove = Window::time(); int width = label->getSize().x; - int realoffset = font->calcWidth(input, caret)-int(textOffset); + uint line = label->getLineByTextIndex(caret); + int offset = label->getLineYOffset(line) + contentOffset().y; + uint lineHeight = font->getLineHeight()*label->getLineInterval(); + scrollStep = lineHeight; + if (offset < 0) { + scrolled(1); + } else if (offset >= getSize().y) { + scrolled(-1); + } + uint lcaret = caret - label->getTextLineOffset(line); + int realoffset = font->calcWidth(input, lcaret)-int(textOffset)+2; if (realoffset-width > 0) { setTextOffset(textOffset + realoffset-width); } else if (realoffset < 0) { diff --git a/src/frontend/gui/controls.h b/src/frontend/gui/controls.h index dbebd9ba..c6e66a41 100644 --- a/src/frontend/gui/controls.h +++ b/src/frontend/gui/controls.h @@ -31,7 +31,12 @@ namespace gui { bool multiline = false; // runtime values + + /// @brief Text Y offset relative to label position + /// (last calculated alignment) int textYOffset = 0; + + /// @brief Text line height multiplied by line interval int totalLineHeight = 1; public: Label(std::string text, std::string fontName="normal"); @@ -43,15 +48,39 @@ namespace gui { virtual void setFontName(std::string name); virtual const std::string& getFontName() const; + /// @brief Set text vertical alignment (default value: center) + /// @param align Align::top / Align::center / Align::bottom virtual void setVerticalAlign(Align align); virtual Align getVerticalAlign() const; + /// @brief Get line height multiplier used for multiline labels + /// (default value: 1.5) virtual float getLineInterval() const; + + /// @brief Set line height multiplier used for multiline labels virtual void setLineInterval(float interval); + /// @brief Get Y position of the text relative to label position + /// @return Y offset virtual int getTextYOffset() const; + + /// @brief Get Y position of the line relative to label position + /// @param line target line index + /// @return Y offset virtual int getLineYOffset(uint line) const; + /// @brief Get position of line start in the text + /// @param line target line index + /// @return position in the text [0..length] + virtual size_t getTextLineOffset(uint line) const; + + /// @brief Get line index by its Y offset relative to label position + /// @param offset target Y offset + /// @return line index [0..+] + virtual uint getLineByYOffset(int offset) const; + virtual uint getLineByTextIndex(size_t index) const; + virtual uint getLinesNumber() const; + virtual void draw(const GfxContext* pctx, Assets* assets) override; virtual void textSupplier(wstringsupplier supplier); @@ -133,8 +162,11 @@ namespace gui { bool valid = true; /// @brief text input pointer, value may be greather than text length uint caret = 0; + /// @brief actual local (line) position of the caret on vertical move + uint maxLocalCaret = 0; uint textOffset = 0; int textInitX; + /// @brief last time of the caret was moved (used for blink animation) double caretLastMove = 0.0; Font* font = nullptr; @@ -146,13 +178,20 @@ namespace gui { size_t normalizeIndex(int index); - int calcIndexAt(int x) const; + int calcIndexAt(int x, int y) const; void paste(const std::wstring& text); void setTextOffset(uint x); void erase(size_t start, size_t length); bool eraseSelected(); void resetSelection(); void extendSelection(int index); + size_t getLineLength(uint line) const; + + /// @brief Get total length of the selection + size_t getSelectionLength() const; + + /// @brief Set maxLocalCaret to local (line) caret position + void resetMaxLocalCaret(); public: TextBox(std::wstring placeholder, glm::vec4 padding=glm::vec4(4.0f)); diff --git a/src/frontend/hud.cpp b/src/frontend/hud.cpp index 60f1396a..41584ab3 100644 --- a/src/frontend/hud.cpp +++ b/src/frontend/hud.cpp @@ -355,7 +355,7 @@ void Hud::update(bool visible) { setPause(true); } } - if (visible && Events::jactive(BIND_HUD_INVENTORY)) { + if (visible && !gui->isFocusCaught() && Events::jactive(BIND_HUD_INVENTORY)) { if (!pause) { if (inventoryOpen) { closeInventory(); diff --git a/src/util/timeutil.cpp b/src/util/timeutil.cpp index e299bec1..bebea0ad 100644 --- a/src/util/timeutil.cpp +++ b/src/util/timeutil.cpp @@ -19,10 +19,6 @@ timeutil::ScopeLogTimer::~ScopeLogTimer() { std::cout << "Scope "<< scopeid_ <<" finished in "<< ScopeLogTimer::stop() << " micros. \n"; } -float timeutil::time_value(float hour, float minute, float second) { - return (hour + (minute + second / 60.0f) / 60.0f) / 24.0f; -} - void timeutil::from_value(float value, int& hour, int& minute, int& second) { value *= 24; hour = value; diff --git a/src/util/timeutil.h b/src/util/timeutil.h index 786d2a91..97f53481 100644 --- a/src/util/timeutil.h +++ b/src/util/timeutil.h @@ -12,12 +12,14 @@ namespace timeutil { int64_t stop(); }; -/* Timer that stops and prints time when destructor called - * @example: - * { // some scope (custom, function, if/else, cycle etc.) - * timeutil::ScopeLogTimer scopeclock(); - * ... - * } */ + /** + * Timer that stops and prints time when destructor called + * @example: + * { // some scope (custom, function, if/else, cycle etc.) + * timeutil::ScopeLogTimer scopeclock(); + * ... + * } + */ class ScopeLogTimer : public Timer{ long long scopeid_; public: @@ -25,7 +27,10 @@ namespace timeutil { ~ScopeLogTimer(); }; - float time_value(float hour, float minute, float second); + inline constexpr float time_value(float hour, float minute, float second) { + return (hour + (minute + second / 60.0f) / 60.0f) / 24.0f; + } + void from_value(float value, int& hour, int& minute, int& second); } diff --git a/src/voxels/ChunksStorage.cpp b/src/voxels/ChunksStorage.cpp index ad251555..4d97de42 100644 --- a/src/voxels/ChunksStorage.cpp +++ b/src/voxels/ChunksStorage.cpp @@ -51,7 +51,7 @@ static void verifyLoadedChunk(ContentIndices* indices, Chunk* chunk) { std::shared_ptr ChunksStorage::create(int x, int z) { World* world = level->getWorld(); - WorldFiles* wfile = world->wfile; + WorldFiles* wfile = world->wfile.get(); auto chunk = std::make_shared(x, z); store(chunk); diff --git a/src/world/Level.cpp b/src/world/Level.cpp index c9f74eee..e1b49712 100644 --- a/src/world/Level.cpp +++ b/src/world/Level.cpp @@ -33,7 +33,7 @@ Level::Level(World* world, const Content* content, EngineSettings& settings) uint matrixSize = (settings.chunks.loadDistance+ settings.chunks.padding) * 2; chunks = new Chunks(matrixSize, matrixSize, 0, 0, - world->wfile, events, content); + world->wfile.get(), events, content); lighting = new Lighting(content, chunks); events->listen(EVT_CHUNK_HIDDEN, [this](lvl_event_type type, Chunk* chunk) { diff --git a/src/world/World.cpp b/src/world/World.cpp index d0da15e7..5cb6cc59 100644 --- a/src/world/World.cpp +++ b/src/world/World.cpp @@ -27,18 +27,18 @@ World::World( uint64_t seed, EngineSettings& settings, const Content* content, - const std::vector packs) - : name(name), - generator(generator), - seed(seed), - settings(settings), - content(content), - packs(packs) { - wfile = new WorldFiles(directory, settings.debug); + const std::vector packs +) : name(name), + generator(generator), + seed(seed), + settings(settings), + content(content), + packs(packs) +{ + wfile = std::make_unique(directory, settings.debug); } World::~World(){ - delete wfile; } void World::updateTimers(float delta) { @@ -73,7 +73,8 @@ Level* World::create(std::string name, uint64_t seed, EngineSettings& settings, const Content* content, - const std::vector& packs) { + const std::vector& packs +) { auto world = new World(name, generator, directory, seed, settings, content, packs); auto level = new Level(world, content, settings); auto inventory = level->player->getInventory(); diff --git a/src/world/World.h b/src/world/World.h index 75af3396..c23ec03d 100644 --- a/src/world/World.h +++ b/src/world/World.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include "../typedefs.h" @@ -36,7 +37,7 @@ class World : Serializable { int64_t nextInventoryId = 1; public: - WorldFiles* wfile; + std::unique_ptr wfile; /** * Day/night loop timer in range 0..1