/******************************************************************************************* * * Text box extended (cursor positioning and editing) * * MODULE USAGE: * #define GUI_TEXTBOX_EXTENDED_IMPLEMENTATION * #include "gui_textbox_extended.h" * * On game draw call: GuiTextBoxEx(...); * * LICENSE: zlib/libpng * * Copyright (c) 2019 Vlad Adrian (@Demizdor) and Ramon Santamaria (@raysan5) * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, including commercial * applications, and to alter it and redistribute it freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not claim that you * wrote the original software. If you use this software in a product, an acknowledgment * in the product documentation would be appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not be misrepresented * as being the original software. * * 3. This notice may not be removed or altered from any source distribution. * **********************************************************************************************/ #ifndef GUI_TEXTBOX_EXTENDED_H #define GUI_TEXTBOX_EXTENDED_H //---------------------------------------------------------------------------------- // Defines and Macros //---------------------------------------------------------------------------------- //... //---------------------------------------------------------------------------------- // Types and Structures Definition //---------------------------------------------------------------------------------- // Text box state data typedef struct GuiTextBoxState { int cursor; // Cursor position in text int start; // Text start position (from where we begin drawing the text) int index; // Text start index (index inside the text of `start` always in sync) int select; // Marks position of cursor when selection has started } GuiTextBoxState; #ifdef __cplusplus extern "C" { // Prevents name mangling of functions #endif //---------------------------------------------------------------------------------- // Module Functions Declaration //---------------------------------------------------------------------------------- RAYGUIDEF void GuiTextBoxSetActive(Rectangle bounds); // Sets the active textbox RAYGUIDEF Rectangle GuiTextBoxGetActive(void); // Get bounds of active textbox RAYGUIDEF void GuiTextBoxSetCursor(int cursor); // Set cursor position of active textbox RAYGUIDEF int GuiTextBoxGetCursor(void); // Get cursor position of active textbox RAYGUIDEF void GuiTextBoxSetSelection(int start, int length); // Set selection of active textbox RAYGUIDEF Vector2 GuiTextBoxGetSelection(void); // Get selection of active textbox (x - selection start y - selection length) RAYGUIDEF bool GuiTextBoxIsActive(Rectangle bounds); // Returns true if a textbox control with specified `bounds` is the active textbox RAYGUIDEF GuiTextBoxState GuiTextBoxGetState(void); // Get state for the active textbox RAYGUIDEF void GuiTextBoxSetState(GuiTextBoxState state); // Set state for the active textbox (state must be valid else things will break) RAYGUIDEF void GuiTextBoxSelectAll(const char *text); // Select all characters in the active textbox (same as pressing `CTRL` + `A`) RAYGUIDEF void GuiTextBoxCopy(const char *text); // Copy selected text to clipboard from the active textbox (same as pressing `CTRL` + `C`) RAYGUIDEF void GuiTextBoxPaste(char *text, int textSize); // Paste text from clipboard into the textbox (same as pressing `CTRL` + `V`) RAYGUIDEF void GuiTextBoxCut(char *text); // Cut selected text in the active textbox and copy it to clipboard (same as pressing `CTRL` + `X`) RAYGUIDEF int GuiTextBoxDelete(char *text, int length, bool before); // Deletes a character or selection before from the active textbox (depending on `before`). Returns bytes deleted. RAYGUIDEF int GuiTextBoxGetByteIndex(const char *text, int start, int from, int to); // Get the byte index for a character starting at position `from` with index `start` until position `to`. RAYGUIDEF bool GuiTextBoxEx(Rectangle bounds, char *text, int textSize, bool editMode); #ifdef __cplusplus } #endif #endif // GUI_TEXTBOX_EXTENDED_H /*********************************************************************************** * * GUI TEXTBOX EXTENDED IMPLEMENTATION * ************************************************************************************/ #if defined(GUI_TEXTBOX_EXTENDED_IMPLEMENTATION) #ifndef RAYGUI_H #include "raygui.h" #endif //---------------------------------------------------------------------------------- // Defines and Macros //---------------------------------------------------------------------------------- //... //---------------------------------------------------------------------------------- // Types and Structures Definition //---------------------------------------------------------------------------------- // Cursor measure mode typedef enum { GUI_MEASURE_MODE_CURSOR_END = 0xA, GUI_MEASURE_MODE_CURSOR_POS, GUI_MEASURE_MODE_CURSOR_COORDS } GuiMeasureMode; //---------------------------------------------------------------------------------- // Global Variables Definition //---------------------------------------------------------------------------------- static Rectangle guiTextBoxActive = { 0 }; // Area of the currently active textbox static GuiTextBoxState guiTextBoxState = { // Keeps state of the active textbox .cursor = -1, .start = 0, .index = 0, .select = -1 }; //---------------------------------------------------------------------------------- // Module Internal Functions Declaration //---------------------------------------------------------------------------------- static int GetPrevCodepoint(const char *text, const char *start, int *prev); static int GuiMeasureTextBox(const char *text, int length, Rectangle rec, int *pos, int mode); static int GuiMeasureTextBoxRev(const char *text, int length, Rectangle rec, int *pos); // Highly synchronized with calculations in DrawTextRecEx() static inline int GuiTextBoxGetCursorCoordinates(const char *text, int length, Rectangle rec, int pos); // Calculate cursor coordinates based on the cursor position `pos` inside the `text`. static inline int GuiTextBoxGetCursorFromMouse(const char *text, int length, Rectangle rec, int *pos); // Calculate cursor position in textbox based on mouse coordinates. static inline int GuiTextBoxMaxCharacters(const char *text, int length, Rectangle rec); // Calculates how many characters is the textbox able to draw inside rec static inline unsigned int GuiCountCodepointsUntilNewline(const char *text); // Returns total number of characters(codepoints) in a UTF8 encoded `text` until `\0` or a `\n` is found. static inline void MoveTextBoxCursorRight(const char *text, int length, Rectangle textRec); static inline void MoveTextBoxCursorLeft(const char *text); static int EncodeCodepoint(unsigned int c, char out[5]); //---------------------------------------------------------------------------------- // Module Functions Definition //---------------------------------------------------------------------------------- // Sets the active textbox (reseting state of the previous active textbox) RAYGUIDEF void GuiTextBoxSetActive(Rectangle bounds) { guiTextBoxActive = bounds; guiTextBoxState = (GuiTextBoxState){ .cursor = -1, .start = 0, .index = 0, .select = -1 }; } // Gets bounds of active textbox RAYGUIDEF Rectangle GuiTextBoxGetActive(void) { return guiTextBoxActive; } // Set cursor position of active textbox RAYGUIDEF void GuiTextBoxSetCursor(int cursor) { guiTextBoxState.cursor = (cursor < 0) ? -1 : cursor; guiTextBoxState.start = -1; // Mark this to be recalculated } // Get cursor position of active textbox RAYGUIDEF int GuiTextBoxGetCursor(void) { return guiTextBoxState.cursor; } // Set selection of active textbox RAYGUIDEF void GuiTextBoxSetSelection(int start, int length) { if (start < 0) start = 0; if (length < 0) length = 0; GuiTextBoxSetCursor(start + length); guiTextBoxState.select = start; } // Get selection of active textbox RAYGUIDEF Vector2 GuiTextBoxGetSelection(void) { if (guiTextBoxState.select == -1 || guiTextBoxState.select == guiTextBoxState.cursor) return RAYGUI_CLITERAL(Vector2){ 0 }; else if (guiTextBoxState.cursor > guiTextBoxState.select) return RAYGUI_CLITERAL(Vector2){ guiTextBoxState.select, guiTextBoxState.cursor - guiTextBoxState.select }; return RAYGUI_CLITERAL(Vector2){ guiTextBoxState.cursor, guiTextBoxState.select - guiTextBoxState.cursor }; } // Returns true if a textbox control with specified `bounds` is the active textbox RAYGUIDEF bool GuiTextBoxIsActive(Rectangle bounds) { return (bounds.x == guiTextBoxActive.x && bounds.y == guiTextBoxActive.y && bounds.width == guiTextBoxActive.width && bounds.height == guiTextBoxActive.height); } RAYGUIDEF GuiTextBoxState GuiTextBoxGetState(void) { return guiTextBoxState; } RAYGUIDEF void GuiTextBoxSetState(GuiTextBoxState state) { // NOTE: should we check if state values are valid ?!? guiTextBoxState = state; } RAYGUIDEF int GuiTextBoxGetByteIndex(const char *text, int start, int from, int to) { int i = start, k = from; while ((text[i] != '\0') && (k < to)) { int j = 0; int letter = GetNextCodepoint(&text[i], &j); if (letter == 0x3f) j = 1; i += j; ++k; } return i; } RAYGUIDEF int GuiTextBoxDelete(char *text, int length, bool before) { if ((guiTextBoxState.cursor != -1) && (text != NULL)) { int startIdx = 0, endIdx = 0; if ((guiTextBoxState.select != -1) && (guiTextBoxState.select != guiTextBoxState.cursor)) { // Delete selection int start = guiTextBoxState.cursor; int end = guiTextBoxState.select; if (guiTextBoxState.cursor > guiTextBoxState.select) { start = guiTextBoxState.select; end = guiTextBoxState.cursor; } // Convert to byte indexes startIdx = GuiTextBoxGetByteIndex(text, 0, 0, start); endIdx = GuiTextBoxGetByteIndex(text, 0, 0, end); // Adjust text box state guiTextBoxState.cursor = start; // Always set cursor to start of selection if (guiTextBoxState.select < guiTextBoxState.start) guiTextBoxState.start = -1; // Force to recalculate on the next frame } else { if (before) { // Delete character before cursor if (guiTextBoxState.cursor != 0) { endIdx = GuiTextBoxGetByteIndex(text, 0, 0, guiTextBoxState.cursor); guiTextBoxState.cursor--; startIdx = GuiTextBoxGetByteIndex(text, 0, 0, guiTextBoxState.cursor); if (guiTextBoxState.cursor < guiTextBoxState.start) guiTextBoxState.start = -1; // Force to recalculate on the next frame } } else { // Delete character after cursor if (guiTextBoxState.cursor + 1 <= (int)GuiCountCodepointsUntilNewline(text)) { startIdx = GuiTextBoxGetByteIndex(text, 0, 0, guiTextBoxState.cursor); endIdx = GuiTextBoxGetByteIndex(text, 0, 0, guiTextBoxState.cursor+1); } } } memmove(&text[startIdx], &text[endIdx], length - endIdx); text[length - (endIdx - startIdx)] = '\0'; guiTextBoxState.select = -1; // Always deselect return (endIdx - startIdx); } return 0; } RAYGUIDEF void GuiTextBoxSelectAll(const char *text) { guiTextBoxState.cursor = GuiCountCodepointsUntilNewline(text); if (guiTextBoxState.cursor > 0) { guiTextBoxState.select = 0; guiTextBoxState.start = -1; // Force recalculate on the next frame } else guiTextBoxState.select = -1; } RAYGUIDEF void GuiTextBoxCopy(const char *text) { if ((text != NULL) && (guiTextBoxState.select != -1) && (guiTextBoxState.cursor != -1) && (guiTextBoxState.select != guiTextBoxState.cursor)) { int start = guiTextBoxState.cursor; int end = guiTextBoxState.select; if (guiTextBoxState.cursor > guiTextBoxState.select) { start = guiTextBoxState.select; end = guiTextBoxState.cursor; } // Convert to byte indexes start = GuiTextBoxGetByteIndex(text, 0, 0, start); end = GuiTextBoxGetByteIndex(text, 0, 0, end); // FIXME: `TextSubtext()` only lets use copy TEXTSPLIT_MAX_TEXT_LENGTH (1024) bytes // maybe modify `SetClipboardText()` so we can use it only on part of a string const char *clipText = TextSubtext(text, start, end - start); SetClipboardText(clipText); } } // Paste text from clipboard into the active textbox. // `text` is the pointer to the buffer used by the textbox while `textSize` is the text buffer max size RAYGUIDEF void GuiTextBoxPaste(char *text, int textSize) { const char *clipText = GetClipboardText(); // GLFW guaratees this should be UTF8 encoded! int length = strlen(text); if ((text != NULL) && (clipText != NULL) && (guiTextBoxState.cursor != -1)) { if ((guiTextBoxState.select != -1) && (guiTextBoxState.select != guiTextBoxState.cursor)) { // If there's a selection we'll have to delete it first length -= GuiTextBoxDelete(text, length, true); } int clipLen = strlen(clipText); // We want the length in bytes // Calculate how many bytes can we copy from clipboard text before we run out of space int size = ((length + clipLen) <= textSize) ? clipLen : textSize - length; // Make room by shifting to right the bytes after cursor int startIdx = GuiTextBoxGetByteIndex(text, 0, 0, guiTextBoxState.cursor); int endIdx = startIdx + size; memmove(&text[endIdx], &text[startIdx], length - startIdx); text[length + size] = '\0'; // Set the NULL char // At long last copy the clipboard text memcpy(&text[startIdx], clipText, size); // Set cursor position at the end of the pasted text guiTextBoxState.cursor = 0; for (int i = 0; i < (startIdx + size); guiTextBoxState.cursor++) { int next = 0; int letter = GetNextCodepoint(&text[i], &next); if (letter != 0x3f) i += next; else i += 1; } guiTextBoxState.start = -1; // Force to recalculate on the next frame } } RAYGUIDEF void GuiTextBoxCut(char* text) { if ((text != NULL) && (guiTextBoxState.select != -1) && (guiTextBoxState.cursor != -1) && (guiTextBoxState.select != guiTextBoxState.cursor)) { // First copy selection to clipboard; int start = guiTextBoxState.cursor, end = guiTextBoxState.select; if (guiTextBoxState.cursor > guiTextBoxState.select) { start = guiTextBoxState.select; end = guiTextBoxState.cursor; } // Convert to byte indexes int startIdx = GuiTextBoxGetByteIndex(text, 0, 0, start); int endIdx = GuiTextBoxGetByteIndex(text, 0, 0, end); // FIXME: `TextSubtext()` only lets use copy TEXTSPLIT_MAX_TEXT_LENGTH (1024) bytes // maybe modify `SetClipboardText()` so we can use it only on parts of a string const char *clipText = TextSubtext(text, startIdx, endIdx - startIdx); SetClipboardText(clipText); // Now delete selection (copy data over it) int len = strlen(text); memmove(&text[startIdx], &text[endIdx], len - endIdx); text[len - (endIdx - startIdx)] = '\0'; // Adjust text box state guiTextBoxState.cursor = start; // Always set cursor to start of selection if (guiTextBoxState.select < guiTextBoxState.start) guiTextBoxState.start = -1; // Force to recalculate guiTextBoxState.select = -1; // Deselect } } // A text box control supporting text selection, cursor positioning and commonly used keyboard shortcuts. // NOTE 1: Requires static variables: framesCounter // NOTE 2: Returns if KEY_ENTER pressed (useful for data validation) RAYGUIDEF bool GuiTextBoxEx(Rectangle bounds, char *text, int textSize, bool editMode) { // Define the cursor movement/selection speed when movement keys are held/pressed #define TEXTBOX_CURSOR_COOLDOWN 5 static int framesCounter = 0; // Required for blinking cursor GuiControlState state = guiState; bool pressed = false; // Make sure length doesn't exceed `textSize`. `textSize` is actually the max amount of characters the textbox can handle. int length = strlen(text); if (length > textSize) { text[textSize] = '\0'; length = textSize; } // Make sure we have enough room to draw at least 1 character if ((bounds.width - 2*GuiGetStyle(TEXTBOX, TEXT_INNER_PADDING)) < GuiGetStyle(DEFAULT, TEXT_SIZE)) { bounds.width = GuiGetStyle(DEFAULT, TEXT_SIZE) + 2*GuiGetStyle(TEXTBOX, TEXT_INNER_PADDING); } // Center the text vertically int verticalPadding = (bounds.height - 2*GuiGetStyle(TEXTBOX, BORDER_WIDTH) - GuiGetStyle(DEFAULT, TEXT_SIZE))/2; if (verticalPadding < 0) { // Make sure the height is sufficient bounds.height = 2*GuiGetStyle(TEXTBOX, BORDER_WIDTH) + GuiGetStyle(DEFAULT, TEXT_SIZE); verticalPadding = 0; } // Calculate the drawing area for the text inside the control `bounds` Rectangle textRec = { bounds.x + GuiGetStyle(TEXTBOX, BORDER_WIDTH) + GuiGetStyle(TEXTBOX, TEXT_INNER_PADDING), bounds.y + verticalPadding + GuiGetStyle(TEXTBOX, BORDER_WIDTH), bounds.width - 2*(GuiGetStyle(TEXTBOX, TEXT_INNER_PADDING) + GuiGetStyle(TEXTBOX, BORDER_WIDTH)), GuiGetStyle(DEFAULT, TEXT_SIZE) }; Vector2 cursorPos = { textRec.x, textRec.y }; // This holds the coordinates inside textRec of the cursor at current position and will be recalculated later bool active = GuiTextBoxIsActive(bounds); // Check if this textbox is the global active textbox int selStart = 0, selLength = 0, textStartIndex = 0; // Update control //-------------------------------------------------------------------- if ((state != GUI_STATE_DISABLED) && !guiLocked) { Vector2 mousePoint = GetMousePosition(); if (editMode) { // Check if we are the global active textbox // A textbox becomes active when the user clicks it :) if (!active) { if (CheckCollisionPointRec(mousePoint, bounds) && (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsMouseButtonPressed(MOUSE_RIGHT_BUTTON))) { // Hurray!!! we just became the active textbox active = true; GuiTextBoxSetActive(bounds); } } else if (!CheckCollisionPointRec(mousePoint, bounds) && IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { // When active and the right mouse is clicked outside the textbox we should deactivate it // NOTE: We set a dummy rect as the active textbox bounds GuiTextBoxSetActive(RAYGUI_CLITERAL(Rectangle){ 0, 0, -1, -1 }); active = false; } if (active) { state = GUI_STATE_PRESSED; framesCounter++; // Make sure state doesn't have invalid values if (guiTextBoxState.cursor > length) guiTextBoxState.cursor = -1; if (guiTextBoxState.select > length) guiTextBoxState.select = -1; if (guiTextBoxState.start > length) guiTextBoxState.start = -1; // Check textbox state for changes and recalculate if necesary if (guiTextBoxState.cursor == -1) { // Set cursor to last visible character in textbox guiTextBoxState.cursor = GuiTextBoxMaxCharacters(text, length, textRec); } if (guiTextBoxState.start == -1) { // Force recalculate text start position and text start index // NOTE: start and index are always in sync // start will hold the starting character position from where the text will be drawn // while index will hold the byte index inside the text for that character if (guiTextBoxState.cursor == 0) { guiTextBoxState.start = guiTextBoxState.index = 0; // No need to recalculate } else { int pos = 0; int len = GuiTextBoxGetByteIndex(text, 0, 0, guiTextBoxState.cursor); guiTextBoxState.index = GuiMeasureTextBoxRev(text, len, textRec, &pos); guiTextBoxState.start = guiTextBoxState.cursor - pos + 1; } } // ----------------- // HANDLE KEY INPUT // ----------------- // * -> | LSHIFT + -> move cursor to the right | increase selection by one // * <- | LSHIFT + <- move cursor to the left | decrease selection by one // * HOME | LSHIFT + HOME moves cursor to start of text | selects text from cursor to start of text // * END | LSHIFT + END move cursor to end of text | selects text from cursor until end of text // * CTRL + A select all characters in text // * CTRL + C copy selected text // * CTRL + X cut selected text // * CTRL + V remove selected text, if any, then paste clipboard data // * DEL delete character or selection after cursor // * BACKSPACE delete character or selection before cursor // TODO: Add more shortcuts (insert mode, select word, moveto/select prev/next word ...) if (IsKeyPressed(KEY_RIGHT) || (IsKeyDown(KEY_RIGHT) && (framesCounter%TEXTBOX_CURSOR_COOLDOWN == 0))) { if (IsKeyDown(KEY_LEFT_SHIFT)) { // Selecting if (guiTextBoxState.select == -1) guiTextBoxState.select = guiTextBoxState.cursor; // Mark selection start MoveTextBoxCursorRight(text, length, textRec); } else { if (guiTextBoxState.select != -1 && guiTextBoxState.select != guiTextBoxState.cursor) { // Deselect and move cursor to end of selection if (guiTextBoxState.cursor < guiTextBoxState.select) { guiTextBoxState.cursor = guiTextBoxState.select - 1; MoveTextBoxCursorRight(text, length, textRec); } } else { // Move cursor to the right MoveTextBoxCursorRight(text, length, textRec); } guiTextBoxState.select = -1; } framesCounter = 0; } else if (IsKeyPressed(KEY_LEFT) || (IsKeyDown(KEY_LEFT) && (framesCounter%TEXTBOX_CURSOR_COOLDOWN == 0))) { if (IsKeyDown(KEY_LEFT_SHIFT)) { // Selecting if (guiTextBoxState.select == -1) guiTextBoxState.select = guiTextBoxState.cursor; // Mark selection start MoveTextBoxCursorLeft(text); } else { if ((guiTextBoxState.select != -1) && (guiTextBoxState.select != guiTextBoxState.cursor)) { // Deselect and move cursor to start of selection if (guiTextBoxState.cursor > guiTextBoxState.select) { guiTextBoxState.cursor = guiTextBoxState.select; if (guiTextBoxState.start > guiTextBoxState.cursor) { guiTextBoxState.start = guiTextBoxState.cursor; guiTextBoxState.index = GuiTextBoxGetByteIndex(text, 0, 0, guiTextBoxState.start); // Recalculate byte index } } } else { // Move cursor to the left MoveTextBoxCursorLeft(text); } guiTextBoxState.select = -1; } framesCounter = 0; } else if (IsKeyPressed(KEY_BACKSPACE) || (IsKeyDown(KEY_BACKSPACE) && (framesCounter%TEXTBOX_CURSOR_COOLDOWN) == 0)) { GuiTextBoxDelete(text, length, true); } else if (IsKeyPressed(KEY_DELETE) || (IsKeyDown(KEY_DELETE) && (framesCounter%TEXTBOX_CURSOR_COOLDOWN) == 0)) { GuiTextBoxDelete(text, length, false); } else if (IsKeyPressed(KEY_HOME)) { if (IsKeyDown(KEY_LEFT_SHIFT)) { // Select from start of text to cursor if ((guiTextBoxState.select > guiTextBoxState.cursor) || ((guiTextBoxState.select == -1) && (guiTextBoxState.cursor != 0))) { guiTextBoxState.select = guiTextBoxState.cursor; } } else guiTextBoxState.select = -1; // Deselect everything // Move cursor to start of text guiTextBoxState.cursor = guiTextBoxState.start = guiTextBoxState.index = 0; framesCounter = 0; } else if (IsKeyPressed(KEY_END)) { int max = GuiCountCodepointsUntilNewline(text); if (IsKeyDown(KEY_LEFT_SHIFT)) { if ((guiTextBoxState.select == -1) && (guiTextBoxState.cursor != max)) { guiTextBoxState.select = guiTextBoxState.cursor; } } else guiTextBoxState.select = -1; // Deselect everything int pos = 0; guiTextBoxState.cursor = max; int len = GuiTextBoxGetByteIndex(text, 0, 0, guiTextBoxState.cursor); guiTextBoxState.index = GuiMeasureTextBoxRev(text, len, textRec, &pos); guiTextBoxState.start = guiTextBoxState.cursor - pos + 1; } else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_A)) GuiTextBoxSelectAll(text); // CTRL + A > Select all else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_C)) GuiTextBoxCopy(text); // CTRL + C > Copy selected text to clipboard else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_X)) GuiTextBoxCut(text); // CTRL + X > Cut selected text else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_V)) GuiTextBoxPaste(text, textSize); // CTRL + V > Paste clipboard text else if (IsKeyPressed(KEY_ENTER)) pressed = true; else { int key = GetKeyPressed(); if ((key >= 32) && ((guiTextBoxState.cursor + 1) < textSize)) { if ((guiTextBoxState.select != -1) && (guiTextBoxState.select != guiTextBoxState.cursor)) { // Delete selection GuiTextBoxDelete(text, length, true); } // Decode codepoint char out[5] = {0}; int sz = EncodeCodepoint(key, &out[0]); if (sz != 0) { int startIdx = GuiTextBoxGetByteIndex(text, 0, 0, guiTextBoxState.cursor); int endIdx = startIdx + sz; if (endIdx <= textSize && length < textSize - 1) { guiTextBoxState.cursor++; guiTextBoxState.select = -1; memmove(&text[endIdx], &text[startIdx], length - startIdx); memcpy(&text[startIdx], &out[0], sz); length += sz; text[length] = '\0'; if (guiTextBoxState.start != -1) { const int max = GuiTextBoxMaxCharacters(&text[guiTextBoxState.index], length - guiTextBoxState.index, textRec); if ((guiTextBoxState.cursor - guiTextBoxState.start) > max) guiTextBoxState.start = -1; } } } } } // ------------- // HANDLE MOUSE // ------------- if (CheckCollisionPointRec(mousePoint, bounds)) { if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (CheckCollisionPointRec(mousePoint, textRec)) { GuiTextBoxGetCursorFromMouse(&text[guiTextBoxState.index], length - guiTextBoxState.index, textRec, &guiTextBoxState.cursor); guiTextBoxState.cursor += guiTextBoxState.start; guiTextBoxState.select = -1; } else { // Clicked outside the `textRec` but still inside bounds if (mousePoint.x <= bounds.x+bounds.width/2) guiTextBoxState.cursor = 0 + guiTextBoxState.start; else guiTextBoxState.cursor = guiTextBoxState.start + GuiTextBoxMaxCharacters(&text[guiTextBoxState.index], length - guiTextBoxState.index, textRec); guiTextBoxState.select = -1; } } else if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { int cursor = guiTextBoxState.cursor - guiTextBoxState.start; bool move = false; if (CheckCollisionPointRec(mousePoint, textRec)) { GuiTextBoxGetCursorFromMouse(&text[guiTextBoxState.index], length - guiTextBoxState.index, textRec, &cursor); } else { // Clicked outside the `textRec` but still inside bounds, this means that we must move the text move = true; if (mousePoint.x > bounds.x+bounds.width/2) { cursor = GuiTextBoxMaxCharacters(&text[guiTextBoxState.index], length - guiTextBoxState.index, textRec); } } guiTextBoxState.cursor = cursor + guiTextBoxState.start; if (guiTextBoxState.select == -1) { // Mark start of selection guiTextBoxState.select = guiTextBoxState.cursor; } // Move the text when cursor is positioned before or after the text if ((framesCounter%TEXTBOX_CURSOR_COOLDOWN) == 0 && move) { if (cursor == 0) MoveTextBoxCursorLeft(text); else if (cursor == GuiTextBoxMaxCharacters(&text[guiTextBoxState.index], length - guiTextBoxState.index, textRec)) { MoveTextBoxCursorRight(text, length, textRec); } } } } // Calculate X coordinate of the blinking cursor cursorPos.x = GuiTextBoxGetCursorCoordinates(&text[guiTextBoxState.index], length - guiTextBoxState.index, textRec, guiTextBoxState.cursor - guiTextBoxState.start); // Update variables textStartIndex = guiTextBoxState.index; if (guiTextBoxState.select == -1) { selStart = guiTextBoxState.cursor; selLength = 0; } else if (guiTextBoxState.cursor > guiTextBoxState.select) { selStart = guiTextBoxState.select; selLength = guiTextBoxState.cursor - guiTextBoxState.select; } else { selStart = guiTextBoxState.cursor; selLength = guiTextBoxState.select - guiTextBoxState.cursor; } // We aren't drawing all of the text so make sure `DrawTextRecEx()` is selecting things correctly if (guiTextBoxState.start > selStart) { selLength -= guiTextBoxState.start - selStart; selStart = 0; } else selStart = selStart - guiTextBoxState.start; } else state = GUI_STATE_FOCUSED; if (IsKeyPressed(KEY_ENTER) || (!CheckCollisionPointRec(mousePoint, bounds) && IsMouseButtonPressed(0))) pressed = true; } else { if (active && IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_C)) { // If active copy all text to clipboard even when disabled // Backup textbox state int select = guiTextBoxState.select; int cursor = guiTextBoxState.cursor; int start = guiTextBoxState.start; if ((guiTextBoxState.select == -1) || (guiTextBoxState.select == guiTextBoxState.cursor)) { // If no selection then mark all text to be copied to clipboard GuiTextBoxSelectAll(text); } GuiTextBoxCopy(text); // Restore textbox state guiTextBoxState.select = select; guiTextBoxState.cursor = cursor; guiTextBoxState.start = start; } if (CheckCollisionPointRec(mousePoint, bounds)) { state = GUI_STATE_FOCUSED; if (IsMouseButtonPressed(0)) pressed = true; } } if (pressed) framesCounter = 0; } // Draw control //-------------------------------------------------------------------- DrawRectangleLinesEx(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), guiAlpha)); if (state == GUI_STATE_PRESSED) { DrawRectangle(bounds.x + GuiGetStyle(TEXTBOX, BORDER_WIDTH), bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH), bounds.width - 2*GuiGetStyle(TEXTBOX, BORDER_WIDTH), bounds.height - 2*GuiGetStyle(TEXTBOX, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_FOCUSED)), guiAlpha)); // Draw blinking cursor if (editMode && active && ((framesCounter/TEXTEDIT_CURSOR_BLINK_FRAMES)%2 == 0) && selLength == 0) { DrawRectangle(cursorPos.x, cursorPos.y, 1, GuiGetStyle(DEFAULT, TEXT_SIZE)*2, Fade(GetColor(GuiGetStyle(TEXTBOX, BORDER_COLOR_PRESSED)), guiAlpha)); } } else if (state == GUI_STATE_DISABLED) { DrawRectangle(bounds.x + GuiGetStyle(TEXTBOX, BORDER_WIDTH), bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH), bounds.width - 2*GuiGetStyle(TEXTBOX, BORDER_WIDTH), bounds.height - 2*GuiGetStyle(TEXTBOX, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_DISABLED)), guiAlpha)); } // Finally draw the text and selection DrawTextRecEx(guiFont, &text[textStartIndex], textRec, GuiGetStyle(DEFAULT, TEXT_SIZE), GuiGetStyle(DEFAULT, TEXT_SPACING), false, Fade(GetColor(GuiGetStyle(TEXTBOX, TEXT + (state*3))), guiAlpha), selStart, selLength, GetColor(GuiGetStyle(TEXTBOX, COLOR_SELECTED_FG)), GetColor(GuiGetStyle(TEXTBOX, COLOR_SELECTED_BG))); return pressed; } //---------------------------------------------------------------------------------- // Module Internal Functions Definition //---------------------------------------------------------------------------------- static int GetPrevCodepoint(const char *text, const char *start, int *prev) { int c = 0x3f; char *p = (char *)text; *prev = 1; for (int i = 0; (p >= start) && (i < 4); p--, i++) { if ((((unsigned char)*p) >> 6) != 2) { c = GetNextCodepoint(p, prev); break; } } return c; } // Returns total number of characters(codepoints) in a UTF8 encoded `text` until `\0` or a `\n` is found. // NOTE: If a invalid UTF8 sequence is encountered a `?`(0x3f) codepoint is counted instead. static inline unsigned int GuiCountCodepointsUntilNewline(const char *text) { unsigned int len = 0; char *ptr = (char*)&text[0]; while ((*ptr != '\0') && (*ptr != '\n')) { int next = 0; int letter = GetNextCodepoint(ptr, &next); if (letter == 0x3f) ptr += 1; else ptr += next; ++len; } return len; } // Highly synchronized with calculations in DrawTextRecEx() static int GuiMeasureTextBox(const char *text, int length, Rectangle rec, int *pos, int mode) { // Get gui font properties const Font font = guiFont; const float fontSize = GuiGetStyle(DEFAULT, TEXT_SIZE); const float spacing = GuiGetStyle(DEFAULT, TEXT_SPACING); int textOffsetX = 0; // Offset between characters float scaleFactor = 0.0f; int letter = 0; // Current character int index = 0; // Index position in sprite font scaleFactor = fontSize/font.baseSize; int i = 0, k = 0; int glyphWidth = 0; for (i = 0; i < length; i++, k++) { glyphWidth = 0; int next = 1; letter = GetNextCodepoint(&text[i], &next); if (letter == 0x3f) next = 1; index = GetGlyphIndex(font, letter); i += next - 1; if (letter != '\n') { glyphWidth = (font.chars[index].advanceX == 0)? (int)(font.recs[index].width*scaleFactor + spacing): (int)(font.chars[index].advanceX*scaleFactor + spacing); if ((textOffsetX + glyphWidth + 1) >= rec.width) break; if ((mode == GUI_MEASURE_MODE_CURSOR_POS) && (*pos == k)) break; else if (mode == GUI_MEASURE_MODE_CURSOR_COORDS) { // Check if the mouse pointer is inside the glyph rect Rectangle grec = {rec.x + textOffsetX - 1, rec.y, glyphWidth, (font.baseSize + font.baseSize/2)*scaleFactor - 1 }; Vector2 mouse = GetMousePosition(); if (CheckCollisionPointRec(mouse, grec)) { // Smooth selection by dividing the glyph rectangle into 2 equal parts and checking where the mouse resides if (mouse.x > (grec.x + glyphWidth/2)) { textOffsetX += glyphWidth; k++; } break; } } } else break; textOffsetX += glyphWidth; } *pos = k; return (rec.x + textOffsetX - 1); } // Required by GuiTextBoxEx() // Highly synchronized with calculations in DrawTextRecEx() static int GuiMeasureTextBoxRev(const char *text, int length, Rectangle rec, int *pos) { // Get gui font properties const Font font = guiFont; const float fontSize = GuiGetStyle(DEFAULT, TEXT_SIZE); const float spacing = GuiGetStyle(DEFAULT, TEXT_SPACING); int textOffsetX = 0; // Offset between characters float scaleFactor = 0.0f; int letter = 0; // Current character int index = 0; // Index position in sprite font scaleFactor = fontSize/font.baseSize; int i = 0, k = 0; int glyphWidth = 0, prev = 1; for (i = length; i >= 0; i--, k++) { glyphWidth = 0; letter = GetPrevCodepoint(&text[i], &text[0], &prev); if (letter == 0x3f) prev = 1; index = GetGlyphIndex(font, letter); i -= prev - 1; if (letter != '\n') { glyphWidth = (font.chars[index].advanceX == 0)? (int)(font.recs[index].width*scaleFactor + spacing): (int)(font.chars[index].advanceX*scaleFactor + spacing); if ((textOffsetX + glyphWidth + 1) >= rec.width) break; } else break; textOffsetX += glyphWidth; } *pos = k; return (i + prev); } // Calculate cursor coordinates based on the cursor position `pos` inside the `text`. static inline int GuiTextBoxGetCursorCoordinates(const char *text, int length, Rectangle rec, int pos) { return GuiMeasureTextBox(text, length, rec, &pos, GUI_MEASURE_MODE_CURSOR_POS); } // Calculate cursor position in textbox based on mouse coordinates. static inline int GuiTextBoxGetCursorFromMouse(const char *text, int length, Rectangle rec, int* pos) { return GuiMeasureTextBox(text, length, rec, pos, GUI_MEASURE_MODE_CURSOR_COORDS); } // Calculates how many characters is the textbox able to draw inside rec static inline int GuiTextBoxMaxCharacters(const char *text, int length, Rectangle rec) { int pos = -1; GuiMeasureTextBox(text, length, rec, &pos, GUI_MEASURE_MODE_CURSOR_END); return pos; } static inline void MoveTextBoxCursorRight(const char* text, int length, Rectangle textRec) { // FIXME: Counting codepoints each time we press the key is expensive, find another way int count = GuiCountCodepointsUntilNewline(text); if (guiTextBoxState.cursor < count ) guiTextBoxState.cursor++; const int max = GuiTextBoxMaxCharacters(&text[guiTextBoxState.index], length - guiTextBoxState.index, textRec); if ((guiTextBoxState.cursor - guiTextBoxState.start) > max) { const int cidx = GuiTextBoxGetByteIndex(text, guiTextBoxState.index, guiTextBoxState.start, guiTextBoxState.cursor); int pos = 0; guiTextBoxState.index = GuiMeasureTextBoxRev(text, cidx - 1, textRec, &pos); guiTextBoxState.start = guiTextBoxState.cursor - pos; } } static inline void MoveTextBoxCursorLeft(const char* text) { if (guiTextBoxState.cursor > 0) guiTextBoxState.cursor--; if (guiTextBoxState.cursor < guiTextBoxState.start) { int prev = 0; int letter = GetPrevCodepoint(&text[guiTextBoxState.index - 1], text, &prev); if (letter == 0x3f) prev = 1; guiTextBoxState.start--; guiTextBoxState.index -= prev; } } static int EncodeCodepoint(unsigned int c, char out[5]) { int len = 0; if (c <= 0x7f) { out[0] = (char)c; len = 1; } else if (c <= 0x7ff) { out[0] = (char)(((c >> 6) & 0x1f) | 0xc0); out[1] = (char)((c & 0x3f) | 0x80); len = 2; } else if (c <= 0xffff) { out[0] = (char)(((c >> 12) & 0x0f) | 0xe0); out[1] = (char)(((c >> 6) & 0x3f) | 0x80); out[2] = (char)((c & 0x3f) | 0x80); len = 3; } else if (c <= 0x10ffff) { out[0] = (char)(((c >> 18) & 0x07) | 0xf0); out[1] = (char)(((c >> 12) & 0x3f) | 0x80); out[2] = (char)(((c >> 6) & 0x3f) | 0x80); out[3] = (char)((c & 0x3f) | 0x80); len = 4; } out[len] = 0; return len; } #endif // GUI_TEXTBOX_EXTENDED_IMPLEMENTATION