|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cstddef>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <string_view>
|
|
|
|
|
|
|
|
|
|
#include <SDL.h>
|
|
|
|
|
|
|
|
|
|
#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
|