You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
430 lines
9.4 KiB
430 lines
9.4 KiB
#pragma once |
|
|
|
#include <algorithm> |
|
#include <cstddef> |
|
#include <cstring> |
|
#include <string> |
|
#include <string_view> |
|
|
|
#ifdef USE_SDL3 |
|
#include <SDL3/SDL_events.h> |
|
#else |
|
#include <SDL.h> |
|
#endif |
|
|
|
#include "utils/utf8.hpp" |
|
|
|
namespace devilution { |
|
|
|
/** @brief A range of bytes in text. */ |
|
struct TextRange { |
|
size_t begin = 0; |
|
size_t end = 0; |
|
|
|
[[nodiscard]] size_t size() const |
|
{ |
|
return end - begin; |
|
} |
|
|
|
[[nodiscard]] bool empty() const |
|
{ |
|
return begin == end; |
|
} |
|
|
|
void clear() |
|
{ |
|
begin = end = 0; |
|
} |
|
}; |
|
|
|
/** |
|
* @brief Current state of the cursor and the selection range. |
|
*/ |
|
struct TextInputCursorState { |
|
size_t position = 0; |
|
TextRange selection; |
|
}; |
|
|
|
/** |
|
* @brief Manages state for a single-line text input with a cursor. |
|
* |
|
* The text value and the cursor position are stored externally. |
|
*/ |
|
class TextInputState { |
|
/** |
|
* @brief Manages an unowned fixed size char array. |
|
*/ |
|
struct Buffer { |
|
Buffer(char *begin, size_t maxLength) |
|
: buf_(begin) |
|
, maxLength_(maxLength) |
|
{ |
|
std::string_view str(begin); |
|
str = TruncateUtf8(str, maxLength); |
|
len_ = str.size(); |
|
buf_[len_] = '\0'; |
|
} |
|
|
|
[[nodiscard]] size_t size() const |
|
{ |
|
return len_; |
|
} |
|
|
|
[[nodiscard]] bool empty() const |
|
{ |
|
return len_ == 0; |
|
} |
|
|
|
Buffer &operator=(std::string_view value) |
|
{ |
|
value = TruncateUtf8(value, maxLength_); |
|
CopyUtf8(buf_, value, maxLength_); |
|
len_ = value.size(); |
|
return *this; |
|
} |
|
|
|
void insert(size_t pos, std::string_view value) |
|
{ |
|
value = truncateForInsertion(value); |
|
std::memmove(&buf_[pos + value.size()], &buf_[pos], len_ - pos); |
|
std::memcpy(&buf_[pos], value.data(), value.size()); |
|
len_ += value.size(); |
|
buf_[len_] = '\0'; |
|
} |
|
|
|
void erase(size_t pos, size_t len) |
|
{ |
|
std::memmove(&buf_[pos], &buf_[pos + len], len_ - (pos + len)); |
|
len_ -= len; |
|
buf_[len_] = '\0'; |
|
} |
|
|
|
void clear() |
|
{ |
|
len_ = 0; |
|
buf_[0] = '\0'; |
|
} |
|
|
|
explicit operator std::string_view() const |
|
{ |
|
return { buf_, len_ }; |
|
} |
|
|
|
private: |
|
/** |
|
* @brief Truncates `text` so that it would fit when inserted, |
|
* respecting UTF-8 code point boundaries. |
|
*/ |
|
[[nodiscard]] std::string_view truncateForInsertion(std::string_view text) const |
|
{ |
|
return TruncateUtf8(text, maxLength_ - len_); |
|
} |
|
|
|
char *buf_; // unowned |
|
size_t maxLength_; |
|
size_t len_; |
|
}; |
|
|
|
public: |
|
struct Options { |
|
char *value; // unowned |
|
TextInputCursorState *cursor; // unowned |
|
size_t maxLength = 0; |
|
}; |
|
TextInputState(const Options &options) |
|
: value_(options.value, options.maxLength) |
|
, cursor_(options.cursor) |
|
{ |
|
cursor_->position = value_.size(); |
|
} |
|
|
|
[[nodiscard]] std::string_view value() const |
|
{ |
|
return std::string_view(value_); |
|
} |
|
|
|
[[nodiscard]] std::string_view selectedText() const |
|
{ |
|
return value().substr(cursor_->selection.begin, cursor_->selection.size()); |
|
} |
|
|
|
[[nodiscard]] bool empty() const |
|
{ |
|
return value_.empty(); |
|
} |
|
|
|
[[nodiscard]] size_t cursorPosition() const |
|
{ |
|
return cursor_->position; |
|
} |
|
|
|
/** |
|
* @brief Overwrites the value with the given text and moves cursor to the end. |
|
*/ |
|
void assign(std::string_view text) |
|
{ |
|
value_ = text; |
|
cursor_->position = value_.size(); |
|
} |
|
|
|
void clear() |
|
{ |
|
value_.clear(); |
|
cursor_->position = 0; |
|
} |
|
|
|
/** |
|
* @brief Truncate to precisely `length` bytes. |
|
*/ |
|
void truncate(size_t length) |
|
{ |
|
if (length >= value().size()) |
|
return; |
|
value_ = value().substr(0, length); |
|
cursor_->position = std::min(cursor_->position, value_.size()); |
|
} |
|
|
|
/** |
|
* @brief Erases the currently selected text and sets the cursor to selection start. |
|
*/ |
|
void eraseSelection() |
|
{ |
|
value_.erase(cursor_->selection.begin, cursor_->selection.size()); |
|
cursor_->position = cursor_->selection.begin; |
|
cursor_->selection.clear(); |
|
} |
|
|
|
/** |
|
* @brief Inserts the text at the current cursor position. |
|
*/ |
|
void type(std::string_view text) |
|
{ |
|
if (!cursor_->selection.empty()) |
|
eraseSelection(); |
|
const size_t prevSize = value_.size(); |
|
value_.insert(cursor_->position, text); |
|
cursor_->position += value_.size() - prevSize; |
|
} |
|
|
|
void backspace(bool word) |
|
{ |
|
if (cursor_->selection.empty()) { |
|
if (cursor_->position == 0) |
|
return; |
|
cursor_->selection.begin = prevPosition(word); |
|
cursor_->selection.end = cursor_->position; |
|
} |
|
eraseSelection(); |
|
} |
|
|
|
void del(bool word) |
|
{ |
|
if (cursor_->selection.empty()) { |
|
if (cursor_->position == value_.size()) |
|
return; |
|
cursor_->selection.begin = cursor_->position; |
|
cursor_->selection.end = nextPosition(word); |
|
} |
|
eraseSelection(); |
|
} |
|
|
|
void setCursorToStart() |
|
{ |
|
cursor_->position = 0; |
|
cursor_->selection.clear(); |
|
} |
|
|
|
void setSelectCursorToStart() |
|
{ |
|
if (cursor_->selection.empty()) { |
|
cursor_->selection.end = cursor_->position; |
|
} else if (cursor_->selection.end == cursor_->position) { |
|
cursor_->selection.end = cursor_->selection.begin; |
|
} |
|
cursor_->selection.begin = cursor_->position = 0; |
|
} |
|
|
|
void setCursorToEnd() |
|
{ |
|
cursor_->position = value_.size(); |
|
cursor_->selection.clear(); |
|
} |
|
|
|
void setSelectCursorToEnd() |
|
{ |
|
if (cursor_->selection.empty()) { |
|
cursor_->selection.begin = cursor_->position; |
|
} else if (cursor_->selection.begin == cursor_->position) { |
|
cursor_->selection.begin = cursor_->selection.end; |
|
} |
|
cursor_->selection.end = cursor_->position = value_.size(); |
|
} |
|
|
|
void moveCursorLeft(bool word) |
|
{ |
|
cursor_->selection.clear(); |
|
if (cursor_->position == 0) |
|
return; |
|
const size_t newPosition = prevPosition(word); |
|
cursor_->position = newPosition; |
|
} |
|
|
|
void moveSelectCursorLeft(bool word) |
|
{ |
|
if (cursor_->position == 0) |
|
return; |
|
const size_t newPosition = prevPosition(word); |
|
if (cursor_->selection.empty()) { |
|
cursor_->selection.begin = newPosition; |
|
cursor_->selection.end = cursor_->position; |
|
} else if (cursor_->selection.end == cursor_->position) { |
|
cursor_->selection.end = newPosition; |
|
} else { |
|
cursor_->selection.begin = newPosition; |
|
} |
|
cursor_->position = newPosition; |
|
} |
|
|
|
void moveCursorRight(bool word) |
|
{ |
|
cursor_->selection.clear(); |
|
if (cursor_->position == value_.size()) |
|
return; |
|
const size_t newPosition = nextPosition(word); |
|
cursor_->position = newPosition; |
|
} |
|
|
|
void moveSelectCursorRight(bool word) |
|
{ |
|
if (cursor_->position == value_.size()) |
|
return; |
|
const size_t newPosition = nextPosition(word); |
|
if (cursor_->selection.empty()) { |
|
cursor_->selection.begin = cursor_->position; |
|
cursor_->selection.end = newPosition; |
|
} else if (cursor_->selection.begin == cursor_->position) { |
|
cursor_->selection.begin = newPosition; |
|
} else { |
|
cursor_->selection.end = newPosition; |
|
} |
|
cursor_->position = newPosition; |
|
} |
|
|
|
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); |
|
} |
|
|
|
[[nodiscard]] std::string_view afterCursor() const |
|
{ |
|
return value().substr(cursor_->position); |
|
} |
|
|
|
Buffer value_; |
|
TextInputCursorState *cursor_; // unowned |
|
}; |
|
|
|
/** |
|
* @brief Manages state for a number input with a cursor. |
|
*/ |
|
class NumberInputState { |
|
public: |
|
struct Options { |
|
TextInputState::Options textOptions; |
|
int min; |
|
int max; |
|
}; |
|
NumberInputState(const Options &options) |
|
: textInput_(options.textOptions) |
|
, min_(options.min) |
|
, max_(options.max) |
|
{ |
|
} |
|
|
|
[[nodiscard]] bool empty() const |
|
{ |
|
return textInput_.empty(); |
|
} |
|
|
|
[[nodiscard]] int value(int defaultValue = 0) const; |
|
|
|
[[nodiscard]] int max() const |
|
{ |
|
return max_; |
|
} |
|
|
|
/** |
|
* @brief Inserts the text at the current cursor position. |
|
* |
|
* Ignores non-numeric characters. |
|
*/ |
|
void type(std::string_view str); |
|
|
|
/** |
|
* @brief Sets the text of the input. |
|
* |
|
* Ignores non-numeric characters. |
|
*/ |
|
void assign(std::string_view str); |
|
|
|
TextInputState &textInput() |
|
{ |
|
return textInput_; |
|
} |
|
|
|
private: |
|
void enforceRange(); |
|
std::string filterStr(std::string_view str, bool allowMinus); |
|
|
|
TextInputState textInput_; |
|
int min_; |
|
int max_; |
|
}; |
|
|
|
bool HandleTextInputEvent(const SDL_Event &event, TextInputState &state); |
|
bool HandleNumberInputEvent(const SDL_Event &event, NumberInputState &state); |
|
|
|
} // namespace devilution
|
|
|