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

#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