From 27b49b79d73f2ef3663a71ac5e90f55c99601ed5 Mon Sep 17 00:00:00 2001 From: AllyTally Date: Wed, 22 Mar 2023 09:09:22 -0300 Subject: [PATCH] Add base text input system --- desktop_version/CMakeLists.txt | 1 + desktop_version/src/KeyPoll.cpp | 2 + desktop_version/src/TextInput.cpp | 385 ++++++++++++++++++++++++++++++ desktop_version/src/TextInput.h | 20 ++ desktop_version/src/UTF8.c | 79 ++++++ desktop_version/src/UTF8.h | 2 + 6 files changed, 489 insertions(+) create mode 100644 desktop_version/src/TextInput.cpp create mode 100644 desktop_version/src/TextInput.h diff --git a/desktop_version/CMakeLists.txt b/desktop_version/CMakeLists.txt index ce3fe90210f..603803e0d6d 100644 --- a/desktop_version/CMakeLists.txt +++ b/desktop_version/CMakeLists.txt @@ -101,6 +101,7 @@ set(VVV_SRC src/Spacestation2.cpp src/TerminalScripts.cpp src/Textbox.cpp + src/TextInput.cpp src/Tower.cpp src/UtilityClass.cpp src/WarpClass.cpp diff --git a/desktop_version/src/KeyPoll.cpp b/desktop_version/src/KeyPoll.cpp index 17d727b7d9f..f1eb0d84a91 100644 --- a/desktop_version/src/KeyPoll.cpp +++ b/desktop_version/src/KeyPoll.cpp @@ -12,6 +12,7 @@ #include "LocalizationStorage.h" #include "Music.h" #include "Screen.h" +#include "TextInput.h" #include "UTF8.h" #include "Vlogging.h" @@ -142,6 +143,7 @@ void KeyPoll::Poll(void) SDL_Event evt; while (SDL_PollEvent(&evt)) { + TextInput::handle_events(evt); switch (evt.type) { /* Keyboard Input */ diff --git a/desktop_version/src/TextInput.cpp b/desktop_version/src/TextInput.cpp new file mode 100644 index 00000000000..6dfdeda3035 --- /dev/null +++ b/desktop_version/src/TextInput.cpp @@ -0,0 +1,385 @@ +#include +#include +#include +#include +#include +#include + +#include "KeyPoll.h" +#include "TextInput.h" +#include "UTF8.h" +#include "UtilityClass.h" +#include "Vlogging.h" + +namespace TextInput { + bool taking_input; + bool selecting; + int flash_timer; + std::vector* current_text; + SDL_Point cursor_pos; + SDL_Point cursor_select_pos; + int cursor_x_tallest; + + void init(void) { + taking_input = false; + } + + void attach_input(std::vector* text) { + taking_input = true; + current_text = text; + selecting = false; + flash_timer = 0; + + send_cursor_to_end(); + } + + void detach_input(void) { + taking_input = false; + } + + void send_cursor_to_end(void) + { + cursor_pos.x = current_text->size() - 1; + cursor_pos.y = current_text->at(cursor_pos.x).size(); + cursor_x_tallest = cursor_pos.x; + } + + void insert_text(std::string text) { + std::stringstream string_stream(text); // Create a stringstream for the text we're inserting + std::string output; // We'll need the output later + while (std::getline(string_stream, output)) { // Iterate through all lines, + output.erase(std::remove(output.begin(), output.end(), '\r'), output.end()); // Strip \r... dammit Windows. + (*current_text)[cursor_pos.y].insert(cursor_pos.x, output); // Insert the current line of text into our text + cursor_pos.x += output.length(); // Update cursor position + if (!string_stream.eof()) { // If we haven't hit the end of the file, + insert_newline(); // Insert a newline + } + } + cursor_x_tallest = cursor_pos.x; + } + + void insert_newline(void) + { + char* first_part = UTF8_substr(current_text->at(cursor_pos.y).c_str(), 0, cursor_pos.x); + char* second_part = UTF8_substr(current_text->at(cursor_pos.y).c_str(), cursor_pos.x, UTF8_total_codepoints(current_text->at(cursor_pos.y).c_str())); + + current_text->at(cursor_pos.y) = first_part; + current_text->insert(current_text->begin() + cursor_pos.y + 1, second_part); + + SDL_free(first_part); + SDL_free(second_part); + + cursor_pos.y++; + cursor_pos.x = 0; + cursor_x_tallest = 0; + } + + void select_all(void) + { + cursor_pos.x = 0; + cursor_pos.y = 0; + cursor_select_pos.x = UTF8_total_codepoints(current_text->at(current_text->size() - 1).c_str()); + cursor_select_pos.y = current_text->size() - 1; + selecting = true; + } + + bool process_selection(void) + { + if (SDL_GetModState() & KMOD_SHIFT) + { + if (!selecting) + { + cursor_select_pos = cursor_pos; + selecting = true; + } + } + else + { + if (selecting) + { + selecting = false; + return true; + } + } + return false; + } + + void move_cursor_up(void) + { + bool reset = process_selection(); // Only returns true if you don't hold shift + if (reset && (cursor_pos.y > cursor_select_pos.y)) { + cursor_pos.y = cursor_select_pos.y; + } + + if (cursor_pos.y > 0) { + cursor_pos.y--; + cursor_pos.x = cursor_x_tallest; + if (cursor_pos.x > UTF8_total_codepoints(current_text->at(cursor_pos.y).c_str())) { + cursor_pos.x = UTF8_total_codepoints(current_text->at(cursor_pos.y).c_str()); + } + } + } + + void move_cursor_down(void) + { + bool reset = process_selection(); // Only returns true if you don't hold shift + if (reset && (cursor_pos.y < cursor_select_pos.y)) { + cursor_pos.y = cursor_select_pos.y; + } + + if (cursor_pos.y < current_text->size() - 1) { + cursor_pos.y++; + cursor_pos.x = cursor_x_tallest; + if (cursor_pos.x > UTF8_total_codepoints(current_text->at(cursor_pos.y).c_str())) { + cursor_pos.x = UTF8_total_codepoints(current_text->at(cursor_pos.y).c_str()); + } + } + } + + void move_cursor_left(void) + { + bool reset = process_selection(); // Only returns true if you don't hold shift + if (reset) { + cursor_pos.x = cursor_select_pos.x; + } + + if (cursor_pos.x > 0) { + cursor_pos.x--; + } + else if (cursor_pos.y > 0) + { + cursor_pos.y--; + cursor_pos.x = UTF8_total_codepoints(current_text->at(cursor_pos.y).c_str()); + } + + cursor_x_tallest = cursor_pos.x; + } + + void move_cursor_right(void) + { + bool reset = process_selection(); // Only returns true if you don't hold shift + if (reset) { + cursor_pos.x = cursor_select_pos.x; + } + + if (cursor_pos.x < UTF8_total_codepoints(current_text->at(cursor_pos.y).c_str())) { + cursor_pos.x++; + } + else if (cursor_pos.y < current_text->size() - 1) + { + cursor_pos.y++; + cursor_pos.x = 0; + } + + cursor_x_tallest = cursor_pos.x; + } + + char* get_selected_text(void) + { + /* Caller must free */ + + Selection_Rect rect = reorder_selection_positions(); + + if (rect.y == rect.y2) { + return UTF8_substr(current_text->at(rect.y).c_str(), rect.x, rect.x2 - rect.x); + } + + char* select_part_first_line = UTF8_substr(current_text->at(rect.y).c_str(), rect.x, UTF8_total_codepoints(current_text->at(rect.y).c_str()) - rect.x); + char* select_part_last_line = UTF8_substr(current_text->at(rect.y2).c_str(), 0, rect.x2); + + // Loop through the lines in between + int total_length = SDL_strlen(select_part_first_line) + SDL_strlen(select_part_last_line) + 1; + for (int i = rect.y + 1; i < rect.y2; i++) { + total_length += SDL_strlen(current_text->at(i).c_str()) + 1; + } + + char* select_part = (char*)SDL_malloc(total_length); + strcpy(select_part, select_part_first_line); + strcat(select_part, "\n"); + for (int i = rect.y + 1; i < rect.y2; i++) { + strcat(select_part, current_text->at(i).c_str()); + strcat(select_part, "\n"); + } + strcat(select_part, select_part_last_line); + + SDL_free(select_part_first_line); + SDL_free(select_part_last_line); + + return select_part; + } + + Selection_Rect reorder_selection_positions(void) { + Selection_Rect positions; + bool in_front = false; + + if (cursor_pos.y > cursor_select_pos.y) { + in_front = true; + } + else if (cursor_pos.y == cursor_select_pos.y) { + if (cursor_pos.x >= cursor_select_pos.x) { + in_front = true; + } + } + + if (in_front) + { + positions.x = cursor_select_pos.x; + positions.x2 = cursor_pos.x; + positions.y = cursor_select_pos.y; + positions.y2 = cursor_pos.y; + } + else + { + positions.x = cursor_pos.x; + positions.x2 = cursor_select_pos.x; + positions.y = cursor_pos.y; + positions.y2 = cursor_select_pos.y; + } + + return positions; + } + + void remove_characters(int x, int y, int x2, int y2) + { + if (x == x2 && y == y2) { + return; + } + // Get the rest of the last line + char* rest_of_string = UTF8_substr(current_text->at(y2).c_str(), x2, UTF8_total_codepoints(current_text->at(y2).c_str()) - x2); + + for (int i = y2; i >= y; i--) + { + if (cursor_pos.y >= i) + { + cursor_pos.y--; + } + if (cursor_select_pos.y >= i) + { + cursor_select_pos.y--; + } + + // Erase the current line + current_text->erase(current_text->begin() + i); + } + + // Erase from the start of the selection to the end + char* erased = UTF8_erase(current_text->at(y).c_str(), x, x2 - x); + current_text->at(y) = erased; + // Add the rest of the last line to the end of the first line + current_text->at(y) += rest_of_string; + SDL_free(erased); + SDL_free(rest_of_string); + } + + void remove_selection(void) + { + Selection_Rect positions = reorder_selection_positions(); + remove_characters(positions.x, positions.y, positions.x2, positions.y2); + selecting = false; + } + + void backspace(void) + { + // The user pressed backspace. + if (selecting) { + remove_selection(); + return; + } + + if (cursor_pos.x == 0) + { + if (cursor_pos.y > 0) + { + // Get the rest of the last line + char* rest_of_string = UTF8_substr(current_text->at(cursor_pos.y).c_str(), 0, UTF8_total_codepoints(current_text->at(cursor_pos.y).c_str())); + + // Erase the current line + current_text->erase(current_text->begin() + cursor_pos.y); + + // Move the cursor up + cursor_pos.y--; + + // Move the cursor to the end of the line + cursor_pos.x = UTF8_total_codepoints(current_text->at(cursor_pos.y).c_str()); + cursor_x_tallest = cursor_pos.x; + + // Add the rest of the last line to the end of the first line + current_text->at(cursor_pos.y) += rest_of_string; + SDL_free(rest_of_string); + } + } + else + { + // Erase the character before the cursor + char* erased = UTF8_erase(current_text->at(cursor_pos.y).c_str(), cursor_pos.x - 1, 1); + current_text->at(cursor_pos.y) = erased; + SDL_free(erased); + + // Move the cursor back + cursor_pos.x--; + cursor_x_tallest = cursor_pos.x; + } + } + + void handle_events(SDL_Event e) { + if (!taking_input) return; + + if (e.type == SDL_KEYDOWN) { + // Handle backspace + if (e.key.keysym.sym == SDLK_BACKSPACE) + { + // Remove the character + backspace(); + } + else if (e.key.keysym.sym == SDLK_v && SDL_GetModState() & KMOD_CTRL) + { + if (selecting) { + remove_selection(); + } + char* clipboard_text = SDL_GetClipboardText(); + insert_text(clipboard_text); + SDL_free(clipboard_text); + } + else if (e.key.keysym.sym == SDLK_c && SDL_GetModState() & KMOD_CTRL) + { + char* selected = get_selected_text(); + SDL_SetClipboardText(selected); + SDL_free(selected); + } + else if (e.key.keysym.sym == SDLK_x && SDL_GetModState() & KMOD_CTRL) + { + char* selected = get_selected_text(); + SDL_SetClipboardText(selected); + SDL_free(selected); + remove_selection(); + } + else if (e.key.keysym.sym == SDLK_a && SDL_GetModState() & KMOD_CTRL) { + select_all(); + } + else if (e.key.keysym.sym == SDLK_RETURN) { + insert_newline(); + } + else if (e.key.keysym.sym == SDLK_UP) { + move_cursor_up(); + } + else if (e.key.keysym.sym == SDLK_DOWN) { + move_cursor_down(); + } + else if (e.key.keysym.sym == SDLK_LEFT) { + move_cursor_left(); + } + else if (e.key.keysym.sym == SDLK_RIGHT) { + move_cursor_right(); + } + } + //Special text input event + else if (e.type == SDL_TEXTINPUT) + { + //Append character(s) + if (selecting) { + remove_selection(); + } + insert_text(e.text.text); + } + } +} diff --git a/desktop_version/src/TextInput.h b/desktop_version/src/TextInput.h new file mode 100644 index 00000000000..96d2f1050e1 --- /dev/null +++ b/desktop_version/src/TextInput.h @@ -0,0 +1,20 @@ +#ifndef TEXTINPUT_H +#define TEXTINPUT_H + +struct Selection_Rect +{ + int x; + int y; + int x2; + int y2; +}; + +namespace TextInput +{ + void send_cursor_to_end(void); + void insert_newline(void); + Selection_Rect reorder_selection_positions(void); + void handle_events(SDL_Event e); +} + +#endif /* TEXTINPUT_H */ diff --git a/desktop_version/src/UTF8.c b/desktop_version/src/UTF8.c index 528b996412e..f1a5c30b636 100644 --- a/desktop_version/src/UTF8.c +++ b/desktop_version/src/UTF8.c @@ -1,5 +1,7 @@ #include "UTF8.h" +#include "SDL.h" + #define STARTS_0(byte) ((byte & 0x80) == 0x00) #define STARTS_10(byte) ((byte & 0xC0) == 0x80) #define STARTS_110(byte) ((byte & 0xE0) == 0xC0) @@ -200,3 +202,80 @@ size_t UTF8_backspace(const char* str, size_t len) return len; } + +char* UTF8_substr(const char* str, size_t start, size_t end) +{ + /* Given a string, return a substring of it. + * The start and end are codepoint indices, not byte indices. + * Caller must VVV_free */ + + const char* start_ptr = str; + const char* end_ptr = str; + + if (end < start) + { + char* substr = SDL_malloc(1); + substr[0] = '\0'; + return substr; + } + + for (size_t i = 0; i < start; i++) + { + if (UTF8_next(&start_ptr) == 0) + { + char* substr = SDL_malloc(1); + substr[0] = '\0'; + return substr; + } + } + + for (size_t i = start; i < end; i++) + { + if (UTF8_next(&end_ptr) == 0) + { + break; + } + } + + size_t len = end_ptr - start_ptr; + char* substr = SDL_malloc(len + 1); + SDL_memcpy(substr, start_ptr, len); + substr[len] = '\0'; + return substr; +} + +char* UTF8_erase(const char* str, size_t start, size_t end) +{ + /* Given a string, return a new string with the given range erased. + * The start and end are codepoint indices, not byte indices. + * Caller must VVV_free */ + + const char* start_ptr = str; + const char* end_ptr = str; + + for (size_t i = 0; i < start; i++) + { + if (UTF8_next(&start_ptr) == 0) + { + char* substr = SDL_malloc(1); + substr[0] = '\0'; + return substr; + } + } + + for (size_t i = start; i < end; i++) + { + if (UTF8_next(&end_ptr) == 0) + { + break; + } + } + + size_t len = SDL_strlen(str); + size_t new_len = len - (end_ptr - start_ptr); + char* new_str = SDL_malloc(new_len + 1); + SDL_memcpy(new_str, str, start_ptr - str); + SDL_memcpy(new_str + (start_ptr - str), end_ptr, len - (end_ptr - str)); + new_str[new_len] = '\0'; + return new_str; +} \ No newline at end of file diff --git a/desktop_version/src/UTF8.h b/desktop_version/src/UTF8.h index 0f9b1cf16f2..f793507f57c 100644 --- a/desktop_version/src/UTF8.h +++ b/desktop_version/src/UTF8.h @@ -27,6 +27,8 @@ UTF8_encoding UTF8_encode(uint32_t codepoint); size_t UTF8_total_codepoints(const char* str); size_t UTF8_backspace(const char* str, size_t len); +char* UTF8_substr(const char* str, size_t start, size_t end); +char* UTF8_erase(const char* str, size_t start, size_t end); #ifdef __cplusplus } /* extern "C" */