Skip to content

Commit

Permalink
Text inputs: Selection, copy, cut
Browse files Browse the repository at this point in the history
Adds shift selection, Ctrl+C, and Ctrl+X handling.

Also fixes left-right movement for non-ASCII characters.
  • Loading branch information
glebm committed Oct 30, 2023
1 parent 98e8d35 commit 55b0fc5
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 59 deletions.
9 changes: 7 additions & 2 deletions Source/DiabloUI/diabloui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ void UiInitList(void (*fnFocus)(int value), void (*fnSelect)(int value), void (*
#endif
UiTextInputState.emplace(TextInputState::Options {
.value = pItemUIEdit->m_value,
.cursorPosition = &pItemUIEdit->m_cursor,
.cursor = &pItemUIEdit->m_cursor,
.maxLength = pItemUIEdit->m_max_length,
});
} else if (item->IsType(UiType::List)) {
Expand Down Expand Up @@ -870,7 +870,12 @@ void Render(const UiEdit &uiEdit)

const Surface &out = Surface(DiabloUiSurface());
DrawString(out, uiEdit.m_value, rect,
{ .flags = uiEdit.GetFlags(), .cursorPosition = static_cast<int>(uiEdit.m_cursor) });
{
.flags = uiEdit.GetFlags(),
.cursorPosition = static_cast<int>(uiEdit.m_cursor.position),
.highlightRange = { static_cast<int>(uiEdit.m_cursor.selection.begin), static_cast<int>(uiEdit.m_cursor.selection.end) },
.highlightColor = 126,
});
}

bool HandleMouseEventArtTextButton(const SDL_Event &event, const UiArtTextButton *uiButton)
Expand Down
27 changes: 23 additions & 4 deletions Source/DiabloUI/text_input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,29 @@ bool HandleInputEvent(const SDL_Event &event, TextInputState &state,
tl::function_ref<bool(std::string_view)> typeFn,
[[maybe_unused]] tl::function_ref<bool(std::string_view)> assignFn)
{
const bool isShift = (SDL_GetModState() & 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) {
const std::string selectedText { state.selectedText() };
if (SDL_SetClipboardText(selectedText.c_str()) < 0) {
Log("{}", SDL_GetError());
}
}
return true;
case SDLK_x:
if ((SDL_GetModState() & KMOD_CTRL) != 0) {
const std::string selectedText { state.selectedText() };
if (SDL_SetClipboardText(selectedText.c_str()) < 0) {
Log("{}", SDL_GetError());
} else {
state.eraseSelection();
}
}
return true;
case SDLK_v:
if ((SDL_GetModState() & KMOD_CTRL) != 0) {
if (SDL_HasClipboardText() == SDL_TRUE) {
Expand All @@ -49,16 +68,16 @@ bool HandleInputEvent(const SDL_Event &event, TextInputState &state,
state.del();
return true;
case SDLK_LEFT:
state.moveCursorLeft();
isShift ? state.moveSelectCursorLeft() : state.moveCursorLeft();
return true;
case SDLK_RIGHT:
state.moveCursorRight();
isShift ? state.moveSelectCursorRight() : state.moveCursorRight();
return true;
case SDLK_HOME:
state.setCursorToStart();
isShift ? state.setSelectCursorToStart() : state.setCursorToStart();
return true;
case SDLK_END:
state.setCursorToEnd();
isShift ? state.setSelectCursorToEnd() : state.setCursorToEnd();
return true;
default:
break;
Expand Down
169 changes: 142 additions & 27 deletions Source/DiabloUI/text_input.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,35 @@

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.
*
Expand Down Expand Up @@ -98,30 +127,35 @@ class TextInputState {

public:
struct Options {
char *value; // unowned
size_t *cursorPosition; // unowned
char *value; // unowned
TextInputCursorState *cursor; // unowned
size_t maxLength = 0;
};
TextInputState(const Options &options)
: value_(options.value, options.maxLength)
, cursorPosition_(options.cursorPosition)
, cursor_(options.cursor)
{
*cursorPosition_ = value_.size();
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 *cursorPosition_;
return cursor_->position;
}

/**
Expand All @@ -130,13 +164,13 @@ class TextInputState {
void assign(std::string_view text)
{
value_ = text;
*cursorPosition_ = value_.size();
cursor_->position = value_.size();
}

void clear()
{
value_.clear();
*cursorPosition_ = 0;
cursor_->position = 0;
}

/**
Expand All @@ -147,72 +181,153 @@ class TextInputState {
if (length >= value().size())
return;
value_ = value().substr(0, length);
*cursorPosition_ = std::min(*cursorPosition_, value_.size());
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(*cursorPosition_, text);
*cursorPosition_ += value_.size() - prevSize;
value_.insert(cursor_->position, text);
cursor_->position += value_.size() - prevSize;
}

void backspace()
{
if (*cursorPosition_ == 0)
return;
const size_t toRemove = *cursorPosition_ - FindLastUtf8Symbols(beforeCursor());
*cursorPosition_ -= toRemove;
value_.erase(*cursorPosition_, toRemove);
if (cursor_->selection.empty()) {
if (cursor_->position == 0)
return;
cursor_->selection.begin = FindLastUtf8Symbols(beforeCursor());
cursor_->selection.end = cursor_->position;
}
eraseSelection();
}

void del()
{
if (*cursorPosition_ == value_.size())
return;
value_.erase(*cursorPosition_, Utf8CodePointLen(afterCursor().data()));
if (cursor_->selection.empty()) {
if (cursor_->position == value_.size())
return;
cursor_->selection.begin = cursor_->position;
cursor_->selection.end = cursor_->position + Utf8CodePointLen(afterCursor().data());
}
eraseSelection();
}

void delSelection()
{
value_.erase(cursor_->selection.begin, cursor_->selection.size());
}

void setCursorToStart()
{
*cursorPosition_ = 0;
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()
{
*cursorPosition_ = value_.size();
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()
{
if (*cursorPosition_ == 0)
cursor_->selection.clear();
if (cursor_->position == 0)
return;
const size_t newPosition = FindLastUtf8Symbols(beforeCursor());
cursor_->position = newPosition;
}

void moveSelectCursorLeft()
{
if (cursor_->position == 0)
return;
--*cursorPosition_;
const size_t newPosition = FindLastUtf8Symbols(beforeCursor());
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()
{
if (*cursorPosition_ == value_.size())
cursor_->selection.clear();
if (cursor_->position == value_.size())
return;
const size_t newPosition = cursor_->position + Utf8CodePointLen(afterCursor().data());
cursor_->position = newPosition;
}

void moveSelectCursorRight()
{
if (cursor_->position == value_.size())
return;
++*cursorPosition_;
const size_t newPosition = cursor_->position + Utf8CodePointLen(afterCursor().data());
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]] std::string_view beforeCursor() const
{
return value().substr(0, *cursorPosition_);
return value().substr(0, cursor_->position);
}

[[nodiscard]] std::string_view afterCursor() const
{
return value().substr(*cursorPosition_);
return value().substr(cursor_->position);
}

Buffer value_;
size_t *cursorPosition_; // unowned
TextInputCursorState *cursor_; // unowned
};

/**
Expand Down
3 changes: 2 additions & 1 deletion Source/DiabloUI/ui_item.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <string>
#include <vector>

#include "DiabloUI/text_input.hpp"
#include "DiabloUI/ui_flags.hpp"
#include "engine/clx_sprite.hpp"
#include "engine/render/text_render.hpp"
Expand Down Expand Up @@ -271,7 +272,7 @@ class UiEdit : public UiItemBase {
std::string_view m_hint;
char *m_value;
std::size_t m_max_length;
size_t m_cursor;
TextInputCursorState m_cursor;
bool m_allowEmpty;
};

Expand Down
Loading

0 comments on commit 55b0fc5

Please sign in to comment.