GuiTextBox() rework (#466)

* GuiTextBox: Fix possible overflow when handling backspace and delete

Simplify code a bit by removing unnecessary checks

* GuiTextBox: Fix CTRL+Backspace behavior

Remove undefined behavior (previously called isspace with oob values)
Fix unable to delete first character
Fix handling of symbols like any other "standard" input field or text editor on Windows does it

* GuiTextBox: Add CTRL+DELETE, CTRL+LEFT and CTRL+RIGHT handling

Copy behavior from programs like notepad, Notepad++, OneNote, address bar in Edge, etc.

* GuiTextBox: Simplify and improve auto-cursor code

Remove one global variable and compact checks into one bool variable
Fix auto cursor triggering immediately when button is held when clicking edit box
Tuned the values, so they match cooldown and delay when entering text

* GuiTextBox: Bring some checks in-line with the rest of the function
This commit is contained in:
foxblock
2025-02-28 15:09:58 +01:00
committed by GitHub
parent c198b9c34e
commit 9a95871701

View File

@ -1395,8 +1395,7 @@ static Rectangle guiControlExclusiveRec = { 0 }; // Gui control exclusive bounds
static int textBoxCursorIndex = 0; // Cursor index, shared by all GuiTextBox*() static int textBoxCursorIndex = 0; // Cursor index, shared by all GuiTextBox*()
//static int blinkCursorFrameCounter = 0; // Frame counter for cursor blinking //static int blinkCursorFrameCounter = 0; // Frame counter for cursor blinking
static int autoCursorCooldownCounter = 0; // Cooldown frame counter for automatic cursor movement on key-down static int autoCursorCounter = 0; // Frame counter for automatic repeated cursor movement on key-down (cooldown and delay)
static int autoCursorDelayCounter = 0; // Delay frame counter for automatic cursor movement
//---------------------------------------------------------------------------------- //----------------------------------------------------------------------------------
// Style data array for all gui style properties (allocated on data segment by default) // Style data array for all gui style properties (allocated on data segment by default)
@ -2489,10 +2488,10 @@ int GuiDropdownBox(Rectangle bounds, const char *text, int *active, bool editMod
int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode) int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode)
{ {
#if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN) #if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN)
#define RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN 40 // Frames to wait for autocursor movement #define RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN 30 // Frames to wait for autocursor movement
#endif #endif
#if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) #if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY)
#define RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY 1 // Frames delay for autocursor movement #define RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY 2 // Frames delay for autocursor movement
#endif #endif
int result = 0; int result = 0;
@ -2526,15 +2525,6 @@ int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode)
mouseCursor.x = -1; mouseCursor.x = -1;
mouseCursor.width = 1; mouseCursor.width = 1;
// Auto-cursor movement logic
// NOTE: Cursor moves automatically when key down after some time
if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_UP) || IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_BACKSPACE) || IsKeyDown(KEY_DELETE)) autoCursorCooldownCounter++;
else
{
autoCursorCooldownCounter = 0; // GLOBAL: Cursor cooldown counter
autoCursorDelayCounter = 0; // GLOBAL: Cursor delay counter
}
// Blink-cursor frame counter // Blink-cursor frame counter
//if (!autoCursorMode) blinkCursorFrameCounter++; //if (!autoCursorMode) blinkCursorFrameCounter++;
//else blinkCursorFrameCounter = 0; //else blinkCursorFrameCounter = 0;
@ -2552,6 +2542,15 @@ int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode)
if (editMode) if (editMode)
{ {
// GLOBAL: Auto-cursor movement logic
// NOTE: Keystrokes are handled repeatedly when button is held down for some time
if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_UP) || IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_BACKSPACE) || IsKeyDown(KEY_DELETE)) autoCursorCounter++;
else
{
autoCursorCounter = 0;
}
bool autoCursorShouldTrigger = (autoCursorCounter > RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN) && ((autoCursorCounter % RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) == 0);
state = STATE_PRESSED; state = STATE_PRESSED;
if (textBoxCursorIndex > textLength) textBoxCursorIndex = textLength; if (textBoxCursorIndex > textLength) textBoxCursorIndex = textLength;
@ -2629,113 +2628,175 @@ int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode)
// Move cursor to end // Move cursor to end
if ((textLength > textBoxCursorIndex) && IsKeyPressed(KEY_END)) textBoxCursorIndex = textLength; if ((textLength > textBoxCursorIndex) && IsKeyPressed(KEY_END)) textBoxCursorIndex = textLength;
// Delete codepoint from text, after current cursor position // Delete related codepoints from text, after current cursor position
if ((textLength > textBoxCursorIndex) && (IsKeyPressed(KEY_DELETE) || (IsKeyDown(KEY_DELETE) && (autoCursorCooldownCounter >= RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN)))) if ((textLength > textBoxCursorIndex) && IsKeyPressed(KEY_DELETE) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL)))
{ {
autoCursorDelayCounter++; int offset = textBoxCursorIndex;
int accCodepointSize = 0;
if (IsKeyPressed(KEY_DELETE) || (autoCursorDelayCounter%RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) == 0) // Delay every movement some frames int nextCodepointSize;
int nextCodepoint;
// Check characters of the same type to delete (either ASCII punctuation or anything non-whitespace)
// Not using isalnum() since it only works on ASCII characters
nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
bool puctuation = ispunct(nextCodepoint & 0xFF);
while (offset < textLength)
{ {
int nextCodepointSize = 0; if ((puctuation && !ispunct(nextCodepoint & 0xFF)) || (!puctuation && (isspace(nextCodepoint & 0xFF) || ispunct(nextCodepoint & 0xFF))))
GetCodepointNext(text + textBoxCursorIndex, &nextCodepointSize); break;
offset += nextCodepointSize;
// Move backward text from cursor position accCodepointSize += nextCodepointSize;
for (int i = textBoxCursorIndex; i < textLength; i++) text[i] = text[i + nextCodepointSize]; nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
textLength -= nextCodepointSize;
if (textBoxCursorIndex > textLength) textBoxCursorIndex = textLength;
// Make sure text last character is EOL
text[textLength] = '\0';
} }
// Check whitespace to delete (ASCII only)
while (offset < textLength)
{
if (!isspace(nextCodepoint & 0xFF))
break;
offset += nextCodepointSize;
accCodepointSize += nextCodepointSize;
nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
}
// Move text after cursor forward (including final null terminator)
for (int i = offset; i <= textLength; i++) text[i - accCodepointSize] = text[i];
textLength -= accCodepointSize;
}
// Delete single codepoint from text, after current cursor position
else if ((textLength > textBoxCursorIndex) && (IsKeyPressed(KEY_DELETE) || (IsKeyDown(KEY_DELETE) && autoCursorShouldTrigger)))
{
int nextCodepointSize = 0;
GetCodepointNext(text + textBoxCursorIndex, &nextCodepointSize);
// Move text after cursor forward (including final null terminator)
for (int i = textBoxCursorIndex + nextCodepointSize; i <= textLength; i++) text[i - nextCodepointSize] = text[i];
textLength -= nextCodepointSize;
} }
// Delete related codepoints from text, before current cursor position // Delete related codepoints from text, before current cursor position
if ((textLength > 0) && IsKeyPressed(KEY_BACKSPACE) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL))) if ((textBoxCursorIndex > 0) && IsKeyPressed(KEY_BACKSPACE) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL)))
{ {
int i = textBoxCursorIndex - 1; int offset = textBoxCursorIndex;
int accCodepointSize = 0; int accCodepointSize = 0;
int prevCodepointSize;
int prevCodepoint;
// Move cursor to the end of word if on space already // Check whitespace to delete (ASCII only)
while ((i > 0) && isspace(text[i])) while (offset > 0)
{ {
int prevCodepointSize = 0; prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize);
GetCodepointPrevious(text + i, &prevCodepointSize); if (!isspace(prevCodepoint & 0xFF))
i -= prevCodepointSize; break;
offset -= prevCodepointSize;
accCodepointSize += prevCodepointSize;
}
// Check characters of the same type to delete (either ASCII punctuation or anything non-whitespace)
// Not using isalnum() since it only works on ASCII characters
bool puctuation = ispunct(prevCodepoint & 0xFF);
while (offset > 0)
{
prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize);
if ((puctuation && !ispunct(prevCodepoint & 0xFF)) || (!puctuation && (isspace(prevCodepoint & 0xFF) || ispunct(prevCodepoint & 0xFF))))
break;
offset -= prevCodepointSize;
accCodepointSize += prevCodepointSize; accCodepointSize += prevCodepointSize;
} }
// Move cursor to the start of the word // Move text after cursor forward (including final null terminator)
while ((i > 0) && !isspace(text[i])) for (int i = textBoxCursorIndex; i <= textLength; i++) text[i - accCodepointSize] = text[i];
{
int prevCodepointSize = 0;
GetCodepointPrevious(text + i, &prevCodepointSize);
i -= prevCodepointSize;
accCodepointSize += prevCodepointSize;
}
// Move forward text from cursor position textLength -= accCodepointSize;
for (int j = (textBoxCursorIndex - accCodepointSize); j < textLength; j++) text[j] = text[j + accCodepointSize]; textBoxCursorIndex -= accCodepointSize;
// Prevent cursor index from decrementing past 0
if (textBoxCursorIndex > 0)
{
textBoxCursorIndex -= accCodepointSize;
textLength -= accCodepointSize;
}
// Make sure text last character is EOL
text[textLength] = '\0';
} }
else if ((textLength > 0) && (IsKeyPressed(KEY_BACKSPACE) || (IsKeyDown(KEY_BACKSPACE) && (autoCursorCooldownCounter >= RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN)))) // Delete single codepoint from text, before current cursor position
else if ((textBoxCursorIndex > 0) && (IsKeyPressed(KEY_BACKSPACE) || (IsKeyDown(KEY_BACKSPACE) && autoCursorShouldTrigger)))
{ {
autoCursorDelayCounter++; int prevCodepointSize = 0;
if (IsKeyPressed(KEY_BACKSPACE) || (autoCursorDelayCounter%RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) == 0) // Delay every movement some frames GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize);
{
int prevCodepointSize = 0;
// Prevent cursor index from decrementing past 0 // Move text after cursor forward (including final null terminator)
if (textBoxCursorIndex > 0) for (int i = textBoxCursorIndex; i <= textLength; i++) text[i - prevCodepointSize] = text[i];
{
GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize);
// Move backward text from cursor position textLength -= prevCodepointSize;
for (int i = (textBoxCursorIndex - prevCodepointSize); i < textLength; i++) text[i] = text[i + prevCodepointSize]; textBoxCursorIndex -= prevCodepointSize;
textBoxCursorIndex -= prevCodepointSize;
textLength -= prevCodepointSize;
}
// Make sure text last character is EOL
text[textLength] = '\0';
}
} }
// Move cursor position with keys // Move cursor position with keys
if (IsKeyPressed(KEY_LEFT) || (IsKeyDown(KEY_LEFT) && (autoCursorCooldownCounter > RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN))) if ((textBoxCursorIndex > 0) && IsKeyPressed(KEY_LEFT) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL)))
{ {
autoCursorDelayCounter++; int offset = textBoxCursorIndex;
int accCodepointSize = 0;
int prevCodepointSize;
int prevCodepoint;
if (IsKeyPressed(KEY_LEFT) || (autoCursorDelayCounter%RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) == 0) // Delay every movement some frames // Check whitespace to skip (ASCII only)
while (offset > 0)
{ {
int prevCodepointSize = 0; prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize);
if (textBoxCursorIndex > 0) GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize); if (!isspace(prevCodepoint & 0xFF))
break;
if (textBoxCursorIndex >= prevCodepointSize) textBoxCursorIndex -= prevCodepointSize; offset -= prevCodepointSize;
accCodepointSize += prevCodepointSize;
} }
// Check characters of the same type to skip (either ASCII punctuation or anything non-whitespace)
// Not using isalnum() since it only works on ASCII characters
bool puctuation = ispunct(prevCodepoint & 0xFF);
while (offset > 0)
{
prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize);
if ((puctuation && !ispunct(prevCodepoint & 0xFF)) || (!puctuation && (isspace(prevCodepoint & 0xFF) || ispunct(prevCodepoint & 0xFF))))
break;
offset -= prevCodepointSize;
accCodepointSize += prevCodepointSize;
}
textBoxCursorIndex = offset;
} }
else if (IsKeyPressed(KEY_RIGHT) || (IsKeyDown(KEY_RIGHT) && (autoCursorCooldownCounter > RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN))) else if ((textBoxCursorIndex > 0) && (IsKeyPressed(KEY_LEFT) || (IsKeyDown(KEY_LEFT) && autoCursorShouldTrigger)))
{ {
autoCursorDelayCounter++; int prevCodepointSize = 0;
GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize);
if (IsKeyPressed(KEY_RIGHT) || (autoCursorDelayCounter%RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) == 0) // Delay every movement some frames textBoxCursorIndex -= prevCodepointSize;
}
else if ((textLength > textBoxCursorIndex) && IsKeyPressed(KEY_RIGHT) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL)))
{
int offset = textBoxCursorIndex;
int accCodepointSize = 0;
int nextCodepointSize;
int nextCodepoint;
// Check characters of the same type to skip (either ASCII punctuation or anything non-whitespace)
// Not using isalnum() since it only works on ASCII characters
nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
bool puctuation = ispunct(nextCodepoint & 0xFF);
while (offset < textLength)
{ {
int nextCodepointSize = 0; if ((puctuation && !ispunct(nextCodepoint & 0xFF)) || (!puctuation && (isspace(nextCodepoint & 0xFF) || ispunct(nextCodepoint & 0xFF))))
GetCodepointNext(text + textBoxCursorIndex, &nextCodepointSize); break;
offset += nextCodepointSize;
if ((textBoxCursorIndex + nextCodepointSize) <= textLength) textBoxCursorIndex += nextCodepointSize; accCodepointSize += nextCodepointSize;
nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
} }
// Check whitespace to skip (ASCII only)
while (offset < textLength)
{
if (!isspace(nextCodepoint & 0xFF))
break;
offset += nextCodepointSize;
accCodepointSize += nextCodepointSize;
nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
}
textBoxCursorIndex = offset;
}
else if ((textLength > textBoxCursorIndex) && (IsKeyPressed(KEY_RIGHT) || (IsKeyDown(KEY_RIGHT) && autoCursorShouldTrigger)))
{
int nextCodepointSize = 0;
GetCodepointNext(text + textBoxCursorIndex, &nextCodepointSize);
textBoxCursorIndex += nextCodepointSize;
} }
// Move cursor position with mouse // Move cursor position with mouse
@ -2791,6 +2852,7 @@ int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode)
(!CheckCollisionPointRec(mousePosition, bounds) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON))) (!CheckCollisionPointRec(mousePosition, bounds) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)))
{ {
textBoxCursorIndex = 0; // GLOBAL: Reset the shared cursor index textBoxCursorIndex = 0; // GLOBAL: Reset the shared cursor index
autoCursorCounter = 0; // GLOBAL: Reset counter for repeated keystrokes
result = 1; result = 1;
} }
} }
@ -2803,6 +2865,7 @@ int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode)
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
{ {
textBoxCursorIndex = textLength; // GLOBAL: Place cursor index to the end of current text textBoxCursorIndex = textLength; // GLOBAL: Place cursor index to the end of current text
autoCursorCounter = 0; // GLOBAL: Reset counter for repeated keystrokes
result = 1; result = 1;
} }
} }