From 84cdb2acb83500ad829f92d6a7a597b06c4d5c0c Mon Sep 17 00:00:00 2001 From: Dan Bechard Date: Mon, 6 Dec 2021 17:03:57 -0500 Subject: [PATCH] Add advanced textbox and input box based on stb_textedit --- src/extras/raygui.h | 713 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 704 insertions(+), 9 deletions(-) diff --git a/src/extras/raygui.h b/src/extras/raygui.h index dfb7792c4..3f7f03488 100644 --- a/src/extras/raygui.h +++ b/src/extras/raygui.h @@ -453,6 +453,10 @@ typedef enum { HUEBAR_SELECTOR_OVERFLOW // Right hue bar selector overflow } GuiColorPickerProperty; +typedef struct { + void *state; // internal state +} GuiTextBoxAdvancedState; + //---------------------------------------------------------------------------------- // Global Variables Definition //---------------------------------------------------------------------------------- @@ -475,6 +479,7 @@ RAYGUIAPI bool GuiIsLocked(void); // Check RAYGUIAPI void GuiFade(float alpha); // Set gui controls alpha (global state), alpha goes from 0.0f to 1.0f RAYGUIAPI void GuiSetState(int state); // Set gui state (global state) RAYGUIAPI int GuiGetState(void); // Get gui state (global state) +RAYGUIAPI void GuiSetActiveTextbox(GuiTextBoxAdvancedState *textbox); // Set active advanced textbox (global state) // Font set/get functions RAYGUIAPI void GuiSetFont(Font font); // Set gui custom font (global state) @@ -504,6 +509,7 @@ RAYGUIAPI bool GuiSpinner(Rectangle bounds, const char *text, int *value, int mi RAYGUIAPI bool GuiValueBox(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode); // Value Box control, updates input text with numbers RAYGUIAPI bool GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode); // Text Box control, updates input text RAYGUIAPI bool GuiTextBoxMulti(Rectangle bounds, char *text, int textSize, bool editMode); // Text Box control with multiple lines +RAYGUIAPI bool GuiTextBoxAdvanced(GuiTextBoxAdvancedState *textboxState, Rectangle bounds, char *text, int *textSize, int textCapacity, bool readOnly); // Advanced textbox with navigation/selection RAYGUIAPI float GuiSlider(Rectangle bounds, const char *textLeft, const char *textRight, float value, float minValue, float maxValue); // Slider control, returns selected value RAYGUIAPI float GuiSliderBar(Rectangle bounds, const char *textLeft, const char *textRight, float value, float minValue, float maxValue); // Slider Bar control, returns selected value RAYGUIAPI float GuiProgressBar(Rectangle bounds, const char *textLeft, const char *textRight, float value, float minValue, float maxValue); // Progress Bar control, shows current progress value @@ -518,6 +524,7 @@ RAYGUIAPI int GuiListView(Rectangle bounds, const char *text, int *scrollIndex, RAYGUIAPI int GuiListViewEx(Rectangle bounds, const char **text, int count, int *focus, int *scrollIndex, int active); // List View with extended parameters RAYGUIAPI int GuiMessageBox(Rectangle bounds, const char *title, const char *message, const char *buttons); // Message Box control, displays a message RAYGUIAPI int GuiTextInputBox(Rectangle bounds, const char *title, const char *message, const char *buttons, char *text); // Text Input Box control, ask for text +RAYGUIAPI int GuiTextInputBoxAdvanced(GuiTextBoxAdvancedState *textboxState, Rectangle bounds, const char *title, const char *message, const char *buttons, int submitButton, char *text, int *textSize, int textCapacity, bool readOnly); // Advanced input box with navigation/selection RAYGUIAPI Color GuiColorPicker(Rectangle bounds, Color color); // Color Picker control (multiple color controls) RAYGUIAPI Color GuiColorPanel(Rectangle bounds, Color color); // Color Panel control RAYGUIAPI float GuiColorBarAlpha(Rectangle bounds, float alpha); // Color Bar Alpha control @@ -1137,6 +1144,152 @@ static unsigned int guiIcons[RICON_MAX_ICONS*RICON_DATA_ELEMENTS] = { #define RAYGUI_MAX_PROPS_BASE 16 // Maximum number of standard properties #define RAYGUI_MAX_PROPS_EXTENDED 8 // Maximum number of extended properties +//---------------------------------------------------------------------------------- +// stb_textedit configuration and type definitions +//---------------------------------------------------------------------------------- +// Symbols that must be the same in header-file and implementation mode: +// +// STB_TEXTEDIT_CHARTYPE the character type +// STB_TEXTEDIT_POSITIONTYPE small type that is a valid cursor position +// STB_TEXTEDIT_UNDOSTATECOUNT the number of undo states to allow +// STB_TEXTEDIT_UNDOCHARCOUNT the number of characters to store in the undo buffer +// +//---------------------------------------------------------------------------------- + +// NOTE(dbechard): It may not be safe to use anything other than "int". These defines were added +// to stb_textedit after-the-fact and there are a few places that e.g. STB_TEXTEDIT_POSITIONTYPE +// interacts with ints in a hard-coded way, which is why I've let these default to int instead of +// specifying a shorter type here (which would reduce the memory overhead of the undo stack). +#if 0 +#define STB_TEXTEDIT_CHARTYPE int +#define STB_TEXTEDIT_KEYTYPE int +#define STB_TEXTEDIT_POSITIONTYPE int +#endif + +// NOTE(dbechrd): These should be user-defined per textbox (requires stb_textedit refactor and +// dynamic allocation for undo stack). +#define STB_TEXTEDIT_UNDOSTATECOUNT 32 +#define STB_TEXTEDIT_UNDOCHARCOUNT 512 + +//---------------------------------------------------------------------------------- +// Symbols you must define for implementation mode: +// +// STB_TEXTEDIT_STRING the type of object representing a string being edited, +// typically this is a wrapper object with other data you need +// +// STB_TEXTEDIT_STRINGLEN(obj) the length of the string (ideally O(1)) +// STB_TEXTEDIT_LAYOUTROW(&r,obj,n) returns the results of laying out a line of characters +// starting from character #n (see discussion below) +// STB_TEXTEDIT_GETWIDTH(obj,n,i) returns the pixel delta from the xpos of the i'th character +// to the xpos of the i+1'th char for a line of characters +// starting at character #n (i.e. accounts for kerning +// with previous char) +// STB_TEXTEDIT_KEYTOTEXT(k) maps a keyboard input to an insertable character +// (return type is int, -1 means not valid to insert) +// STB_TEXTEDIT_GETCHAR(obj,i) returns the i'th character of obj, 0-based +// STB_TEXTEDIT_NEWLINE the character returned by _GETCHAR() we recognize +// as manually wordwrapping for end-of-line positioning +// +// STB_TEXTEDIT_DELETECHARS(obj,i,n) delete n characters starting at i +// STB_TEXTEDIT_INSERTCHARS(obj,i,c*,n) insert n characters at i (pointed to by STB_TEXTEDIT_CHARTYPE*) +// +//---------------------------------------------------------------------------------- +#define STB_TEXTEDIT_STRING rstb_String +#define STB_TEXTEDIT_STRINGLEN(str) rstb_StringLen(str) +#define STB_TEXTEDIT_LAYOUTROW(row, str, start) rstb_LayoutRow(row, str, start) +#define STB_TEXTEDIT_GETWIDTH(str, start, offset) rstb_GetWidth(str, start + offset) +#define STB_TEXTEDIT_KEYTOTEXT(keycode) rstb_KeyToText(keycode) +#define STB_TEXTEDIT_GETCHAR(str, index) rstb_GetChar(str, index) +#define STB_TEXTEDIT_NEWLINE ((int)'\n') +#define STB_TEXTEDIT_DELETECHARS(str, start, count) rstb_DeleteChars(str, start, count) +#define STB_TEXTEDIT_INSERTCHARS(str, start, chars, count) rstb_InsertChars(str, start, chars, count) +#define STB_TEXTEDIT_IS_SPACE(c) rstb_IsSpace(c) + +//---------------------------------------------------------------------------------- +// +// STB_TEXTEDIT_K_SHIFT a power of two that is or'd in to a keyboard input to represent the shift keycode +// +// STB_TEXTEDIT_K_LEFT keyboard input to move cursor left +// STB_TEXTEDIT_K_RIGHT keyboard input to move cursor right +// STB_TEXTEDIT_K_UP keyboard input to move cursor up +// STB_TEXTEDIT_K_DOWN keyboard input to move cursor down +// STB_TEXTEDIT_K_LINESTART keyboard input to move cursor to start of line // e.g. HOME +// STB_TEXTEDIT_K_LINEEND keyboard input to move cursor to end of line // e.g. END +// STB_TEXTEDIT_K_TEXTSTART keyboard input to move cursor to start of text // e.g. ctrl-HOME +// STB_TEXTEDIT_K_TEXTEND keyboard input to move cursor to end of text // e.g. ctrl-END +// STB_TEXTEDIT_K_DELETE keyboard input to delete selection or character under cursor +// STB_TEXTEDIT_K_BACKSPACE keyboard input to delete selection or character left of cursor +// STB_TEXTEDIT_K_UNDO keyboard input to perform undo +// STB_TEXTEDIT_K_REDO keyboard input to perform redo +// +//---------------------------------------------------------------------------------- +#define KMOD_SHIFT (1 << 10) // 1024, bit flag to represent shift modifier +#define KMOD_CTRL (1 << 11) // 2048, bit flag to represent control modifier + +#define STB_TEXTEDIT_K_SHIFT KMOD_SHIFT +#define STB_TEXTEDIT_K_LEFT KEY_LEFT +#define STB_TEXTEDIT_K_RIGHT KEY_RIGHT +#define STB_TEXTEDIT_K_UP KEY_UP +#define STB_TEXTEDIT_K_DOWN KEY_DOWN +#define STB_TEXTEDIT_K_LINESTART KEY_HOME +#define STB_TEXTEDIT_K_LINEEND KEY_END +#define STB_TEXTEDIT_K_TEXTSTART (KMOD_CTRL | KEY_HOME) +#define STB_TEXTEDIT_K_TEXTEND (KMOD_CTRL | KEY_END) +#define STB_TEXTEDIT_K_DELETE KEY_DELETE +#define STB_TEXTEDIT_K_BACKSPACE KEY_BACKSPACE +#define STB_TEXTEDIT_K_UNDO (KMOD_CTRL | KEY_Z) +#define STB_TEXTEDIT_K_REDO (KMOD_CTRL | KMOD_SHIFT | KEY_Z) + +//---------------------------------------------------------------------------------- +// Optional: +// STB_TEXTEDIT_K_INSERT keyboard input to toggle insert mode +// STB_TEXTEDIT_IS_SPACE(ch) true if character is whitespace (e.g. 'isspace'), +// required for default WORDLEFT/WORDRIGHT handlers +// STB_TEXTEDIT_MOVEWORDLEFT(obj,i) custom handler for WORDLEFT, returns index to move cursor to +// STB_TEXTEDIT_MOVEWORDRIGHT(obj,i) custom handler for WORDRIGHT, returns index to move cursor to +// STB_TEXTEDIT_K_WORDLEFT keyboard input to move cursor left one word // e.g. ctrl-LEFT +// STB_TEXTEDIT_K_WORDRIGHT keyboard input to move cursor right one word // e.g. ctrl-RIGHT +// STB_TEXTEDIT_K_LINESTART2 secondary keyboard input to move cursor to start of line +// STB_TEXTEDIT_K_LINEEND2 secondary keyboard input to move cursor to end of line +// STB_TEXTEDIT_K_TEXTSTART2 secondary keyboard input to move cursor to start of text +// STB_TEXTEDIT_K_TEXTEND2 secondary keyboard input to move cursor to end of text +//---------------------------------------------------------------------------------- +//#define STB_TEXTEDIT_K_INSERT KEY_INSERT // who actually uses insert mode..? disabled for now +//#define STB_TEXTEDIT_MOVEWORDLEFT // see: stb_textedit_move_to_word_previous +//#define STB_TEXTEDIT_MOVEWORDRIGHT // see: stb_textedit_move_to_word_next +#define STB_TEXTEDIT_K_WORDLEFT (KMOD_CTRL | KEY_LEFT) +#define STB_TEXTEDIT_K_WORDRIGHT (KMOD_CTRL | KEY_RIGHT) +//#define STB_TEXTEDIT_K_LINESTART2 +//#define STB_TEXTEDIT_K_LINEEND2 +//#define STB_TEXTEDIT_K_TEXTSTART2 +//#define STB_TEXTEDIT_K_TEXTEND2 + +#include "stb_textedit.h" + +typedef struct { + char *buffer; // text edit buffer + // TODO(dbechrd): Characters, not bytes, need to handle this correctly for Unicode + int used; // # of characters in use + int capacity; // size of buffer in bytes, last byte reserverd for nil terminator +} rstb_String; + +static int rstb_StringLen(rstb_String *str); +static void rstb_LayoutRow(StbTexteditRow *row, rstb_String *str, int start); +static float rstb_GetWidth(rstb_String *str, int index); +static int rstb_KeyToText(int key);; +static int rstb_GetChar(rstb_String *str, int index); +static void rstb_DeleteChars(rstb_String *str, int start, int count); +static int rstb_InsertChars(rstb_String *str, int start, int *chars, int count); +static int rstb_IsSpace(int c); + +#define STB_TEXTEDIT_IMPLEMENTATION +#include "stb_textedit.h" + +typedef struct { + STB_TexteditState stb_state; + bool focused; +} rGuiTextBoxAdvancedState; + //---------------------------------------------------------------------------------- // Types and Structures Definition //---------------------------------------------------------------------------------- @@ -1168,6 +1321,163 @@ static unsigned int guiStyle[RAYGUI_MAX_CONTROLS*(RAYGUI_MAX_PROPS_BASE + RAYGUI static bool guiStyleLoaded = false; // Style loaded flag for lazy style initialization +static GuiTextBoxAdvancedState *guiActiveTextbox = 0; + +//---------------------------------------------------------------------------------- +// stb_textedit function implementations +//---------------------------------------------------------------------------------- +#include + +static int rstb_StringLen(rstb_String *str) { + return str->used; + //return (int)strnlen(str, str->textSize); +} + +static void rstb_LayoutRow(StbTexteditRow *row, rstb_String *str, int start) +{ + // float x0,x1; // starting x location, end x location (allows for align=right, etc) + // float baseline_y_delta; // position of baseline relative to previous row's baseline + // float ymin,ymax; // height of row above and below baseline + // int num_chars; + + // HACK(dbechrd): Assuming single-line mode + UNUSED(start); + assert(start == 0); + + Vector2 textSize = MeasureTextEx(guiFont, str->buffer, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); + row->x0 = 0.0f; + row->x1 = textSize.x; + row->baseline_y_delta = (float)guiFont.baseSize; + // TODO(dbechrd): Not sure what STB's coordinate system is here... might need to swap these or make one negative + // for multi-line mode to work properly. + row->ymin = 0.0f; + row->ymax = (float)guiFont.baseSize; + row->num_chars = str->used; +} + +static float rstb_GetWidth(rstb_String *str, int index) +{ + // Get next codepoint from byte string and glyph index in font + assert(index < str->used); + + int codepointByteCount = 0; + int codepoint = GetCodepoint(&str->buffer[index], &codepointByteCount); + int glpyhIndex = GetGlyphIndex(guiFont, codepoint); + + // TODO: scaleFactor and spacing should probably be configurable? + float scaleFactor = 1.0f; + float spacing = 1.0f; + float glyphWidth = (float)guiFont.glyphs[glpyhIndex].advanceX*scaleFactor + spacing; + return glyphWidth; +} + +static int rstb_KeyToText(int key) +{ + // Clear modifier keycode flags + int c = key & ~(KMOD_CTRL | KMOD_SHIFT); + + // TODO(dbechrd): What should the filter be? + if (c < 32 || c >= 128) + { + c = -1; // not valid to insert + } + return c; +} + +static int rstb_GetChar(rstb_String *str, int index) +{ + assert(index >= 0); + assert(index < str->used); + + // TODO: Unicode (requires O(n) traversal) + int c = (int)str->buffer[index]; + return c; +} + +static void rstb_DeleteChars(rstb_String *str, int start, int count) +{ + assert(start >= 0); + assert(count > 0); + assert(start + count <= str->used); + + // TODO(dbechrd): "start" and "count" are probably meant to be Unicode character indices, not byte indices + + // NOTE: If moveBytes is 0, we're deleting from end of string and don't need to shift anything left + int moveBytes = str->used - (start + count); + if (moveBytes > 0) + { + // Shift tail of string left to overwrite deleted section + char *dst = str->buffer + start; + char *src = str->buffer + start + count; + for (int i = 0; i < moveBytes; i++) + { + *dst = *src; + dst++; + src++; + } + } + str->used -= count; + + // Clear deleted/shifted characters at end of string + assert(str->used + count < str->capacity); + memset(str->buffer + str->used, 0, count); +} + +static int rstb_InsertChars(rstb_String *str, int start, int *chars, int count) +{ + assert(start >= 0); + assert(count > 0); + assert(str->used + count < str->capacity); + + int success = 0; + // TODO(dbechrd): Unicode support, currently assumes ASCII + if (count + str->used < str->capacity) + { + if (start == str->used) + { + // Inserting at end of string is easy, just append + for (int i = 0; i < count; i++) + { + assert(chars[i] >= 32); + assert(chars[i] < 128); + str->buffer[str->used] = (char)chars[i]; + } + } + else + { + // Shift text at insertion location to the right + char *dst = str->buffer + str->used + count - 1; + char *src = str->buffer + str->used + count - 2; + int moveBytes = str->used - (start + count - 1); + for (int i = moveBytes; i > 0; i--) + { + *dst = *src; + dst--; + src--; + } + + // Copy new text into the gap we just created + memcpy(str->buffer + start, chars, count); + } + success = 1; + } + + str->used += count; + assert(str->used < str->capacity); + return success; +} + +static int rstb_IsSpace(int c) +{ + int isSpace = 0; + switch (c) + { + case ' ': case '\t': case '\r': case '\n': + isSpace = 1; + } + return isSpace; +} + //---------------------------------------------------------------------------------- // Standalone Mode Functions Declaration // @@ -1239,6 +1549,7 @@ static void DrawRectangleGradientV(int posX, int posY, int width, int height, Co // Module specific Functions Declaration //---------------------------------------------------------------------------------- static int GetTextWidth(const char *text); // Gui get text width using default font +static int GetTextSubstringWidth(rstb_String *str, int start, int length); // Get text width of a substring of an STB string static Rectangle GetTextBounds(int control, Rectangle bounds); // Get text bounds considering control bounds static const char *GetTextIcon(const char *text, int *iconId); // Get text icon if provided and move text cursor @@ -1282,6 +1593,8 @@ void GuiSetState(int state) { guiState = (GuiControlState)state; } // Get gui state (global state) int GuiGetState(void) { return guiState; } +void GuiSetActiveTextbox(GuiTextBoxAdvancedState *textbox) { guiActiveTextbox = textbox; } + // Set custom gui font // NOTE: Font loading/unloading is external to raygui void GuiSetFont(Font font) @@ -2042,6 +2355,250 @@ bool GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode) return pressed; } +// Text Box control, updates input text +// NOTE 1: Requires static variables: framesCounter +// NOTE 2: Returns if KEY_ENTER validate (useful for data validation) +bool GuiTextBoxAdvanced(GuiTextBoxAdvancedState *textboxState, Rectangle bounds, char *text, int *textSize, int textCapacity, bool readOnly) +{ + assert(text); + assert(textSize && *textSize >= 0); + assert(textCapacity > 0); + assert(*textSize < textCapacity); + + static int framesCounter = 0; // Required for blinking cursor + static double lastKeyPress = 0; + static int keyRepeat = 0; + + // TODO: This has to be per-textbox, but storing it here during prototyping to keep dependency internal to raygui + if (!textboxState->state) { + textboxState->state = RAYGUI_CALLOC(1, sizeof(rGuiTextBoxAdvancedState)); + } + rGuiTextBoxAdvancedState *rTextboxState = ((rGuiTextBoxAdvancedState *)textboxState->state); + if (!rTextboxState->stb_state.initialized || *textSize == 0) { + stb_textedit_initialize_state(&rTextboxState->stb_state, 1); + } + + rstb_String str = { 0 }; + str.buffer = text; + str.used = *textSize; + str.capacity = textCapacity; + + GuiControlState state = guiState; + bool submit = false; + + Rectangle textBounds = GetTextBounds(TEXTBOX, bounds); + +#if 0 + Rectangle cursor = { + bounds.x + GuiGetStyle(TEXTBOX, TEXT_PADDING) + GetTextWidth(text) + 2, + bounds.y + GuiGetStyle(DEFAULT, TEXT_SIZE), + 1, + (float)GuiGetStyle(DEFAULT, TEXT_SIZE) * 1.5f + }; +#endif + + // Update control + //-------------------------------------------------------------------- + if ((state != GUI_STATE_DISABLED) && !guiLocked && !readOnly) { + Vector2 mousePoint = GetMousePosition(); + + state = GUI_STATE_NORMAL; + + // Check for mouse click on/off control, and mouse hover + if (CheckCollisionPointRec(mousePoint, bounds)) { + state = GUI_STATE_FOCUSED; + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + // User clicked on this control + guiActiveTextbox = textboxState; + } + } else if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + // User clicked somewhere else + if (guiActiveTextbox == textboxState) { + guiActiveTextbox = 0; + } + framesCounter = 0; + } + + // Handle input when textbox in edit mode + if (guiActiveTextbox == textboxState && !readOnly) { + state = GUI_STATE_PRESSED; + framesCounter++; + + int ctrlDown = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL); + int shiftDown = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); + int keyMods = (ctrlDown > 0 ? KMOD_CTRL : 0) | (shiftDown > 0 ? KMOD_SHIFT : 0); + + int codepoint = GetCharPressed(); // Returns codepoint as Unicode + while (codepoint) { + // TODO(dbechrd): What is max value returned by GetCharPressed()? Are these bits safe to overwrite? + assert(codepoint < KMOD_CTRL); + assert(codepoint < KMOD_SHIFT); + stb_textedit_key(&str, &rTextboxState->stb_state, codepoint | keyMods); + codepoint = GetCharPressed(); + } + + bool left = IsKeyDown(KEY_LEFT); + bool right = IsKeyDown(KEY_RIGHT); + if (left && right) { + left = false; + right = false; + } + + bool up = IsKeyDown(KEY_UP); + bool down = IsKeyDown(KEY_DOWN); + if (up && down) { + up = false; + down = false; + } + + bool home = IsKeyDown(KEY_HOME); + bool end = IsKeyDown(KEY_END); + if (home && end) { + home = false; + end = false; + } + + bool del = IsKeyDown(KEY_DELETE); + bool backspace = IsKeyDown(KEY_BACKSPACE); + if (del && backspace) { + del = false; + backspace = false; + } + + bool z = IsKeyDown(KEY_Z); + bool insert = IsKeyDown(KEY_INSERT); + + bool any = left || right || up || down || home || end || del || backspace || z || insert; + if (any) { + const double delayBeforeRepeat = 0.15; + const double delayDuringRepeat = 0.03; + + const double timeSinceLastKeyPress = GetTime() - lastKeyPress; + //if (timeSinceLastKeyPress > delayBeforeRepeat * 2) + //{ + // keyRepeat = 0; + //} + + double keyRepeatDelay = 0; + if (keyRepeat > 1) { + keyRepeatDelay = delayDuringRepeat; + } else if (keyRepeat == 1) { + keyRepeatDelay = delayBeforeRepeat; + } + + if (timeSinceLastKeyPress > keyRepeatDelay) { + if (left) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_LEFT); + if (right) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_RIGHT); + if (up) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_UP); + if (down) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_DOWN); + if (home) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_HOME); + if (end) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_END); + if (del) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_DELETE); + if (backspace) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_BACKSPACE); + if (z) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_INSERT); + if (insert) stb_textedit_key(&str, &rTextboxState->stb_state, keyMods | KEY_Z); + keyRepeat++; + lastKeyPress = GetTime(); + } + framesCounter = 0; + } else { + keyRepeat = 0; + } + + if (ctrlDown && IsKeyPressed(KEY_A)) { + rTextboxState->stb_state.select_start = 0; + rTextboxState->stb_state.select_end = str.used; + } + + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + stb_textedit_click(&str, &rTextboxState->stb_state, (float)GetMouseX() - textBounds.x, (float)GetMouseY() - textBounds.y); + } else if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { + stb_textedit_drag(&str, &rTextboxState->stb_state, (float)GetMouseX() - textBounds.x, (float)GetMouseY() - textBounds.y); + } + + // If enter key was pressed, we're done editing + if (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER)) { + framesCounter = 0; + submit = true; + } + +#if 0 + // Check text alignment to position cursor properly + int textAlignment = GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT); + if (textAlignment == GUI_TEXT_ALIGN_CENTER) cursor.x = bounds.x + GetTextWidth(text) / 2 + bounds.width / 2 + 1; + else if (textAlignment == GUI_TEXT_ALIGN_RIGHT) cursor.x = bounds.x + bounds.width - GuiGetStyle(TEXTBOX, TEXT_INNER_PADDING); +#else + // TODO: Check text alignment to position cursor properly +#endif + } + } + //-------------------------------------------------------------------- + + assert(str.used < str.capacity); + text[str.used] = '\0'; + + // Draw control + //-------------------------------------------------------------------- + int selectionStart = MIN(rTextboxState->stb_state.select_start, rTextboxState->stb_state.select_end); + int selectionLength = ABS(rTextboxState->stb_state.select_end - rTextboxState->stb_state.select_start); + + GuiControlProperty baseColorProperty = BASE_COLOR_NORMAL; + switch (state) { + case GUI_STATE_PRESSED: baseColorProperty = BASE_COLOR_PRESSED; break; + case GUI_STATE_DISABLED: baseColorProperty = BASE_COLOR_DISABLED; break; + case GUI_STATE_FOCUSED: baseColorProperty = BASE_COLOR_FOCUSED; break; + } + + GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), + //Fade(GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), guiAlpha), + //Fade(GetColor(GuiGetStyle(TEXTBOX, baseColorProperty)), guiAlpha) + Fade(BLACK, 0.8f), + Fade({ 50, 50, 50, 255 }, 0.6f) + ); + + if (guiActiveTextbox == textboxState) { + if (selectionLength) { + // Draw selection highlight + Color selectedHighlight = ORANGE; //(Color){ 52, 105, 218, 255 }; + int selectionOffsetX = GetTextSubstringWidth(&str, 0, selectionStart); + int selectionWidth = GetTextSubstringWidth(&str, selectionStart, selectionLength); + Rectangle cursor = { + bounds.x + GuiGetStyle(TEXTBOX, TEXT_PADDING) + selectionOffsetX + 2, + bounds.y + bounds.height / 2 - GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(TEXTBOX, TEXT_PADDING), + (float)selectionWidth, + (float)GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(TEXTBOX, TEXT_PADDING) / 2 + }; + + GuiDrawRectangle(cursor, 0, BLANK, Fade(selectedHighlight, guiAlpha)); + } else { + // Draw blinking cursor + int cursorOffetX = GetTextSubstringWidth(&str, 0, rTextboxState->stb_state.cursor); + Rectangle cursor = { + bounds.x + GuiGetStyle(TEXTBOX, TEXT_PADDING) + cursorOffetX + 2, + bounds.y + bounds.height / 2 - GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(TEXTBOX, TEXT_PADDING), + 1, + (float)GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(TEXTBOX, TEXT_PADDING) / 2 + }; + + if (!readOnly && ((framesCounter / 20) % 2 == 0)) { + //GuiDrawRectangle(cursor, 0, BLANK, Fade(GetColor(GuiGetStyle(TEXTBOX, BORDER_COLOR_PRESSED)), guiAlpha)); + GuiDrawRectangle(cursor, 0, BLANK, Fade(RAYWHITE, guiAlpha)); + } + } + } + + //Color textColor = GetColor(GuiGetStyle(TEXTBOX, TEXT + (state * 3))); + Color textColor = RAYWHITE; + if (selectionLength) { + //textColor = RAYWHITE; + } + GuiDrawText(text, textBounds, GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT), Fade(textColor, guiAlpha)); + //-------------------------------------------------------------------- + + *textSize = str.used; + return submit; +} + // Spinner control, returns selected value bool GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode) { @@ -2079,6 +2636,14 @@ bool GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, in } } +#if defined(RAYGUI_NO_RICONS) + if (GuiButton(leftButtonBound, "<")) tempValue--; + if (GuiButton(rightButtonBound, ">")) tempValue++; +#else + if (GuiButton(leftButtonBound, GuiIconText(RICON_ARROW_LEFT_FILL, NULL))) tempValue--; + if (GuiButton(rightButtonBound, GuiIconText(RICON_ARROW_RIGHT_FILL, NULL))) tempValue++; +#endif + if (!editMode) { if (tempValue < minValue) tempValue = minValue; @@ -2098,13 +2663,7 @@ bool GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, in GuiSetStyle(BUTTON, BORDER_WIDTH, GuiGetStyle(SPINNER, BORDER_WIDTH)); GuiSetStyle(BUTTON, TEXT_ALIGNMENT, GUI_TEXT_ALIGN_CENTER); -#if defined(RAYGUI_NO_RICONS) - if (GuiButton(leftButtonBound, "<")) tempValue--; - if (GuiButton(rightButtonBound, ">")) tempValue++; -#else - if (GuiButton(leftButtonBound, GuiIconText(RICON_ARROW_LEFT_FILL, NULL))) tempValue--; - if (GuiButton(rightButtonBound, GuiIconText(RICON_ARROW_RIGHT_FILL, NULL))) tempValue++; -#endif + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlign); GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth); @@ -2184,6 +2743,9 @@ bool GuiValueBox(Rectangle bounds, const char *text, int *value, int minValue, i if (valueHasChanged) *value = TextToInteger(textValue); + if (*value > maxValue) *value = maxValue; + else if (*value < minValue) *value = minValue; + if (IsKeyPressed(KEY_ENTER) || (!CheckCollisionPointRec(mousePoint, bounds) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON))) pressed = true; } else @@ -2349,7 +2911,7 @@ bool GuiTextBoxMulti(Rectangle bounds, char *text, int textSize, bool editMode) { int glyphWidth = 0; if (glyphInfo.advanceX != 0) glyphWidth += glyphInfo.advanceX; - else glyphWidth += (atlasRec.width + glyphInfo.offsetX); + else glyphWidth += (int)(atlasRec.width + glyphInfo.offsetX); // Jump line if the end of the text box area has been reached if ((cursorPos.x + (glyphWidth*scaleFactor)) > (textAreaBounds.x + textAreaBounds.width)) @@ -2382,7 +2944,7 @@ bool GuiTextBoxMulti(Rectangle bounds, char *text, int textSize, bool editMode) int glyphWidth = 0; if (glyphInfo.advanceX != 0) glyphWidth += glyphInfo.advanceX; - else glyphWidth += (atlasRec.width + glyphInfo.offsetX); + else glyphWidth += (int)(atlasRec.width + glyphInfo.offsetX); cursorPos.x += (glyphWidth*scaleFactor + (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); //if (i > lastSpacePos) lastSpaceWidth += (atlasRec.width + (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); @@ -3219,6 +3781,108 @@ int GuiTextInputBox(Rectangle bounds, const char *title, const char *message, co return btnIndex; } +// Text Input Box control, ask for text +int GuiTextInputBoxAdvanced(GuiTextBoxAdvancedState *textboxState, Rectangle bounds, const char *title, const char *message, const char *buttons, int submitButton, char *text, int *textLength, int textCapacity, bool readOnly) +{ + int btnIndex = -1; + const float fontSize = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + const float margin = 4.0f; + const float buttonSpacing = 4.0f; + const float buttonPadding = (float)GuiGetStyle(BUTTON, TEXT_PADDING); + const float buttonHeight = fontSize + buttonPadding * 2; + + Vector2 messageSize = { 0 }; + float messageWidth = 0.0f; + if (message != NULL) { + messageSize = MeasureTextEx(guiFont, message, fontSize, 1); + messageWidth = messageSize.x + buttonPadding * 2; + } + + int buttonsCount = 0; + const char **buttonsText = GuiTextSplit(buttons, &buttonsCount, NULL); + + float widestButton = 0; + for (int i = 0; i < buttonsCount; i++) { + float buttonWidth = MeasureTextEx(guiFont, buttonsText[i], fontSize, 1).x; + widestButton = MAX(widestButton, buttonWidth + buttonPadding * 2); + } + + const float buttonRowWidth = (float)GuiGetStyle(STATUSBAR, BORDER_WIDTH) * 2 + margin * 2 + widestButton * buttonsCount + buttonSpacing * (buttonsCount - 1); + const float minWidth = MAX(messageWidth, buttonRowWidth); + if (minWidth > bounds.width) { + bounds.width = minWidth; + } + + float contextX = bounds.x + (float)GuiGetStyle(STATUSBAR, BORDER_WIDTH) + margin; + float contentY = bounds.y + (float)GuiGetStyle(STATUSBAR, BORDER_WIDTH) + WINDOW_STATUSBAR_HEIGHT + margin; + float contentWidth = bounds.width - (float)GuiGetStyle(STATUSBAR, BORDER_WIDTH) * 2 - margin * 2; + + Rectangle messageBounds = { 0 }; + if (message != NULL) { + messageSize = MeasureTextEx(guiFont, message, fontSize, 1); + messageBounds.x = contextX; + messageBounds.y = contentY; + messageBounds.width = messageWidth; + messageBounds.height = messageSize.y; + contentY += messageBounds.height + margin; + } + + Rectangle textBoxBounds = { 0 }; + textBoxBounds.x = contextX; + textBoxBounds.y = contentY; + textBoxBounds.width = contentWidth; + textBoxBounds.height = fontSize + (float)GuiGetStyle(TEXTBOX, TEXT_PADDING) * 2; + contentY += textBoxBounds.height + buttonPadding; + + contentY += buttonHeight + buttonPadding; + float minHeight = contentY + (float)GuiGetStyle(STATUSBAR, BORDER_WIDTH) + margin - bounds.y; + if (minHeight > bounds.height) { + bounds.height = minHeight; + } + + // NOTE: Buttons are anchored to bottom, so this has to happen after minHeight calculation + Rectangle buttonBounds = { 0 }; + buttonBounds.x = contextX + contentWidth - widestButton * buttonsCount - buttonSpacing * (buttonsCount - 1); + buttonBounds.y = bounds.y + bounds.height - GuiGetStyle(STATUSBAR, BORDER_WIDTH) - buttonHeight - margin; + buttonBounds.width = widestButton; + buttonBounds.height = buttonHeight; + + // Draw control + //-------------------------------------------------------------------- + if (GuiWindowBox(bounds, title)) btnIndex = 0; + + // Draw message if available + if (message != NULL) { + //int prevTextAlignment = GuiGetStyle(LABEL, TEXT_ALIGNMENT); + //GuiSetStyle(LABEL, TEXT_ALIGNMENT, GUI_TEXT_ALIGN_CENTER); + GuiLabel(messageBounds, message); + //GuiSetStyle(LABEL, TEXT_ALIGNMENT, prevTextAlignment); + } + + if (GuiTextBoxAdvanced(textboxState, textBoxBounds, text, textLength, textCapacity, readOnly)) { + btnIndex = submitButton; + } + + int prevBtnTextAlignment = GuiGetStyle(BUTTON, TEXT_ALIGNMENT); + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, GUI_TEXT_ALIGN_CENTER); + + for (int i = 0; i < buttonsCount; i++) { + if (GuiButton(buttonBounds, buttonsText[i])) { + btnIndex = i + 1; + } + buttonBounds.x += (buttonBounds.width + buttonSpacing); + } + + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, prevBtnTextAlignment); + //-------------------------------------------------------------------- + + if (btnIndex >= 0 && guiActiveTextbox == textboxState) { + guiActiveTextbox = 0; + } + + return btnIndex; +} + // Grid control // NOTE: Returns grid mouse-hover selected cell // About drawing lines at subpixel spacing, simple put, not easy solution: @@ -3738,6 +4402,37 @@ static int GetTextWidth(const char *text) return (int)size.x; } +// Get text width of a substring of an STB string +static int GetTextSubstringWidth(rstb_String *str, int start, int length) +{ + assert(start >= 0); + assert(length >= 0); + assert(start + length <= str->used); + + if (length == 0) { + return 0; + } + + // HACK(dbechrd): Save character at cursor so that we can replace it with nil and call GetTextWidth() to + // get the width of a partial string + // TODO: Implement MeasureTextSubstring() that takes a starting index and offset, and only measures the + // substring (should it return early if it encounters '\0'? .. probably) + char charAtCursor = 0; + int end = start + length; + if (end < str->used) { + charAtCursor = str->buffer[end]; + str->buffer[end] = '\0'; + } + + int width = GetTextWidth(str->buffer + start); + + if (end < str->capacity) { + str->buffer[end] = charAtCursor; + } + + return width; +} + // Get text bounds considering control bounds static Rectangle GetTextBounds(int control, Rectangle bounds) {