From 2b0876edfa1c146d523f30cfa3ddfca60361fd95 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Wed, 25 Oct 2023 17:35:32 +0100 Subject: [PATCH] Text input: Add Ctrl/Alt word navigation --- Source/DiabloUI/text_input.cpp | 19 +++++---- Source/DiabloUI/text_input.hpp | 71 ++++++++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/Source/DiabloUI/text_input.cpp b/Source/DiabloUI/text_input.cpp index fe6743af96e..79199ec17a7 100644 --- a/Source/DiabloUI/text_input.cpp +++ b/Source/DiabloUI/text_input.cpp @@ -25,13 +25,16 @@ bool HandleInputEvent(const SDL_Event &event, TextInputState &state, tl::function_ref typeFn, [[maybe_unused]] tl::function_ref assignFn) { - const bool isShift = (SDL_GetModState() & KMOD_SHIFT) != 0; + const auto modState = SDL_GetModState(); + const bool isCtrl = (modState & KMOD_CTRL) != 0; + const bool isAlt = (modState & KMOD_ALT) != 0; + const bool isShift = (modState & KMOD_SHIFT) != 0; switch (event.type) { case SDL_KEYDOWN: { switch (event.key.keysym.sym) { #ifndef USE_SDL1 case SDLK_c: - if ((SDL_GetModState() & KMOD_CTRL) != 0) { + if (isCtrl) { const std::string selectedText { state.selectedText() }; if (SDL_SetClipboardText(selectedText.c_str()) < 0) { Log("{}", SDL_GetError()); @@ -39,7 +42,7 @@ bool HandleInputEvent(const SDL_Event &event, TextInputState &state, } return true; case SDLK_x: - if ((SDL_GetModState() & KMOD_CTRL) != 0) { + if (isCtrl) { const std::string selectedText { state.selectedText() }; if (SDL_SetClipboardText(selectedText.c_str()) < 0) { Log("{}", SDL_GetError()); @@ -49,7 +52,7 @@ bool HandleInputEvent(const SDL_Event &event, TextInputState &state, } return true; case SDLK_v: - if ((SDL_GetModState() & KMOD_CTRL) != 0) { + if (isCtrl) { if (SDL_HasClipboardText() == SDL_TRUE) { std::unique_ptr> clipboard { SDL_GetClipboardText() }; if (clipboard == nullptr || *clipboard == '\0') { @@ -62,16 +65,16 @@ bool HandleInputEvent(const SDL_Event &event, TextInputState &state, return true; #endif case SDLK_BACKSPACE: - state.backspace(); + state.backspace(/*word=*/isCtrl || isAlt); return true; case SDLK_DELETE: - state.del(); + state.del(/*word=*/isCtrl || isAlt); return true; case SDLK_LEFT: - isShift ? state.moveSelectCursorLeft() : state.moveCursorLeft(); + isShift ? state.moveSelectCursorLeft(/*word=*/isCtrl || isAlt) : state.moveCursorLeft(/*word=*/isCtrl || isAlt); return true; case SDLK_RIGHT: - isShift ? state.moveSelectCursorRight() : state.moveCursorRight(); + isShift ? state.moveSelectCursorRight(/*word=*/isCtrl || isAlt) : state.moveCursorRight(/*word=*/isCtrl || isAlt); return true; case SDLK_HOME: isShift ? state.setSelectCursorToStart() : state.setCursorToStart(); diff --git a/Source/DiabloUI/text_input.hpp b/Source/DiabloUI/text_input.hpp index da46037bad5..1e05785d563 100644 --- a/Source/DiabloUI/text_input.hpp +++ b/Source/DiabloUI/text_input.hpp @@ -206,33 +206,28 @@ class TextInputState { cursor_->position += value_.size() - prevSize; } - void backspace() + void backspace(bool word) { if (cursor_->selection.empty()) { if (cursor_->position == 0) return; - cursor_->selection.begin = FindLastUtf8Symbols(beforeCursor()); + cursor_->selection.begin = prevPosition(word); cursor_->selection.end = cursor_->position; } eraseSelection(); } - void del() + void del(bool word) { if (cursor_->selection.empty()) { if (cursor_->position == value_.size()) return; cursor_->selection.begin = cursor_->position; - cursor_->selection.end = cursor_->position + Utf8CodePointLen(afterCursor().data()); + cursor_->selection.end = nextPosition(word); } eraseSelection(); } - void delSelection() - { - value_.erase(cursor_->selection.begin, cursor_->selection.size()); - } - void setCursorToStart() { cursor_->position = 0; @@ -265,20 +260,20 @@ class TextInputState { cursor_->selection.end = cursor_->position = value_.size(); } - void moveCursorLeft() + void moveCursorLeft(bool word) { cursor_->selection.clear(); if (cursor_->position == 0) return; - const size_t newPosition = FindLastUtf8Symbols(beforeCursor()); + const size_t newPosition = prevPosition(word); cursor_->position = newPosition; } - void moveSelectCursorLeft() + void moveSelectCursorLeft(bool word) { if (cursor_->position == 0) return; - const size_t newPosition = FindLastUtf8Symbols(beforeCursor()); + const size_t newPosition = prevPosition(word); if (cursor_->selection.empty()) { cursor_->selection.begin = newPosition; cursor_->selection.end = cursor_->position; @@ -290,20 +285,20 @@ class TextInputState { cursor_->position = newPosition; } - void moveCursorRight() + void moveCursorRight(bool word) { cursor_->selection.clear(); if (cursor_->position == value_.size()) return; - const size_t newPosition = cursor_->position + Utf8CodePointLen(afterCursor().data()); + const size_t newPosition = nextPosition(word); cursor_->position = newPosition; } - void moveSelectCursorRight() + void moveSelectCursorRight(bool word) { if (cursor_->position == value_.size()) return; - const size_t newPosition = cursor_->position + Utf8CodePointLen(afterCursor().data()); + const size_t newPosition = nextPosition(word); if (cursor_->selection.empty()) { cursor_->selection.begin = cursor_->position; cursor_->selection.end = newPosition; @@ -316,6 +311,48 @@ class TextInputState { } private: + [[nodiscard]] static bool isWordSeparator(unsigned char c) + { + const bool isAsciiWordChar = (c >= '0' && c <= '9') + || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; + return c <= '\x7E' && !isAsciiWordChar; + } + + [[nodiscard]] size_t prevPosition(bool word) const + { + const std::string_view str = beforeCursor(); + size_t pos = FindLastUtf8Symbols(str); + if (!word) + return pos; + while (pos > 0 && isWordSeparator(str[pos])) { + pos = FindLastUtf8Symbols({ str.data(), pos }); + } + while (pos > 0) { + const size_t prevPos = FindLastUtf8Symbols({ str.data(), pos }); + if (isWordSeparator(str[prevPos])) + break; + pos = prevPos; + } + return pos; + } + + [[nodiscard]] size_t nextPosition(bool word) const + { + const std::string_view str = afterCursor(); + size_t pos = Utf8CodePointLen(str.data()); + if (!word) + return cursor_->position + pos; + while (pos < str.size() && isWordSeparator(str[pos])) { + pos += Utf8CodePointLen(str.data() + pos); + } + while (pos < str.size()) { + pos += Utf8CodePointLen(str.data() + pos); + if (isWordSeparator(str[pos])) + break; + } + return cursor_->position + pos; + } + [[nodiscard]] std::string_view beforeCursor() const { return value().substr(0, cursor_->position);