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.
 
 
 
 
 
 

278 lines
5.5 KiB

#pragma once
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <string>
#include <string_view>
#include <SDL.h>
#include "utils/utf8.hpp"
namespace devilution {
/**
* @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);
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
{
const size_t newLength = len_ + text.size();
if (newLength > maxLength_) {
return TruncateUtf8(text, newLength - maxLength_);
}
return text;
}
char *buf_; // unowned
size_t maxLength_;
size_t len_;
};
public:
struct Options {
char *value; // unowned
size_t *cursorPosition; // unowned
size_t maxLength = 0;
};
TextInputState(const Options &options)
: value_(options.value, options.maxLength)
, cursorPosition_(options.cursorPosition)
{
*cursorPosition_ = value_.size();
}
[[nodiscard]] std::string_view value() const
{
return std::string_view(value_);
}
[[nodiscard]] bool empty() const
{
return value_.empty();
}
[[nodiscard]] size_t cursorPosition() const
{
return *cursorPosition_;
}
/**
* @brief Overwrites the value with the given text and moves cursor to the end.
*/
void assign(std::string_view text)
{
value_ = text;
*cursorPosition_ = value_.size();
}
void clear()
{
value_.clear();
*cursorPosition_ = 0;
}
/**
* @brief Truncate to precisely `length` bytes.
*/
void truncate(size_t length)
{
if (length >= value().size())
return;
value_ = value().substr(0, length);
*cursorPosition_ = std::min(*cursorPosition_, value_.size());
}
/**
* @brief Inserts the text at the current cursor position.
*/
void type(std::string_view text)
{
const size_t prevSize = value_.size();
value_.insert(*cursorPosition_, text);
*cursorPosition_ += value_.size() - prevSize;
}
void backspace()
{
if (*cursorPosition_ == 0)
return;
const size_t toRemove = *cursorPosition_ - FindLastUtf8Symbols(beforeCursor());
*cursorPosition_ -= toRemove;
value_.erase(*cursorPosition_, toRemove);
}
void del()
{
if (*cursorPosition_ == value_.size())
return;
value_.erase(*cursorPosition_, Utf8CodePointLen(afterCursor().data()));
}
void setCursorToStart()
{
*cursorPosition_ = 0;
}
void setCursorToEnd()
{
*cursorPosition_ = value_.size();
}
void moveCursorLeft()
{
if (*cursorPosition_ == 0)
return;
--*cursorPosition_;
}
void moveCursorRight()
{
if (*cursorPosition_ == value_.size())
return;
++*cursorPosition_;
}
private:
[[nodiscard]] std::string_view beforeCursor() const
{
return value().substr(0, *cursorPosition_);
}
[[nodiscard]] std::string_view afterCursor() const
{
return value().substr(*cursorPosition_);
}
Buffer value_;
size_t *cursorPosition_; // 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