Mercurial
diff third_party/raylib/include/raygui.h @ 173:827c6ac504cd hg-web
Merged in default here.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 19 Jan 2026 18:59:10 -0800 |
| parents | 058de208e640 |
| children |
line wrap: on
line diff
--- a/third_party/raylib/include/raygui.h Sat Jan 10 13:35:09 2026 -0800 +++ b/third_party/raylib/include/raygui.h Mon Jan 19 18:59:10 2026 -0800 @@ -479,6 +479,13 @@ } GuiTextStyle; */ +// Result from JUNE_GuiTextBoxEx - includes selection info +typedef struct JUNE_TextBoxResult { + int result; // Original return value (0 = no change, 1 = mode changed) + int selectionStart; // Start index of selection (-1 if no selection) + int selectionEnd; // End index of selection (-1 if no selection) +} JUNE_TextBoxResult; + // Gui control state typedef enum { STATE_NORMAL = 0, @@ -736,6 +743,7 @@ // Basic controls set RAYGUIAPI int GuiLabel(Rectangle bounds, const char *text); // Label control RAYGUIAPI int GuiButton(Rectangle bounds, const char *text); // Button control, returns true when clicked +RAYGUIAPI int GuiButtonRounded(Rectangle bounds, const char *text, float roundness, int segments); RAYGUIAPI int GuiLabelButton(Rectangle bounds, const char *text); // Label button control, returns true when clicked RAYGUIAPI int GuiToggle(Rectangle bounds, const char *text, bool *active); // Toggle Button control RAYGUIAPI int GuiToggleGroup(Rectangle bounds, const char *text, int *active); // Toggle Group control @@ -744,6 +752,7 @@ RAYGUIAPI int GuiComboBox(Rectangle bounds, const char *text, int *active); // Combo Box control RAYGUIAPI int GuiDropdownBox(Rectangle bounds, const char *text, int *active, bool editMode); // Dropdown Box control +RAYGUIAPI int GuiDropdownBoxRounded(Rectangle bounds, const char *text, int *active, bool editMode, float roundness, int segments); RAYGUIAPI int GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode); // Spinner control RAYGUIAPI int GuiValueBox(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode); // Value Box control, updates input text with numbers RAYGUIAPI int GuiValueBoxFloat(Rectangle bounds, const char *text, char *textValue, float *value, bool editMode); // Value box control for float values @@ -1410,6 +1419,9 @@ static int textBoxCursorIndex = 0; // Cursor index, shared by all GuiTextBox*() //static int blinkCursorFrameCounter = 0; // Frame counter for cursor blinking static int autoCursorCounter = 0; // Frame counter for automatic repeated cursor movement on key-down (cooldown and delay) +static int textBoxSelectionStart = -1; // Selection start index (-1 if no selection) +static int textBoxSelectionEnd = -1; // Selection end index (-1 if no selection) +static bool textBoxSelecting = false; // Currently selecting with mouse //---------------------------------------------------------------------------------- // Style data array for all gui style properties (allocated on data segment by default) @@ -2029,6 +2041,40 @@ return result; // Button pressed: result = 1 } +// JUNE +int GuiButtonRounded(Rectangle bounds, const char *text, float roundness, int segments) +{ + int result = 0; + GuiState state = guiState; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GetMousePosition(); + + // Check button state + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) state = STATE_PRESSED; + else state = STATE_FOCUSED; + + if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) result = 1; + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + DrawRectangleRounded(bounds, roundness, segments, GetColor(GuiGetStyle(BUTTON, BASE + (state*3)))); + GuiDrawText(text, GetTextBounds(BUTTON, bounds), GuiGetStyle(BUTTON, TEXT_ALIGNMENT), GetColor(GuiGetStyle(BUTTON, TEXT + (state*3)))); + + if (state == STATE_FOCUSED) GuiTooltip(bounds); + //------------------------------------------------------------------ + + return result; // Button pressed: result = 1 +} + // Label button control int GuiLabelButton(Rectangle bounds, const char *text) { @@ -2541,6 +2587,178 @@ return result; // Mouse click: result = 1 } +// JUNE +int GuiDropdownBoxRounded(Rectangle bounds, const char *text, int *active, bool editMode, float roundness, int segments) +{ + int result = 0; + GuiState state = guiState; + + int temp = 0; + if (active == NULL) active = &temp; + + int itemSelected = *active; + int itemFocused = -1; + + int direction = 0; // Dropdown box open direction: down (default) + if (GuiGetStyle(DROPDOWNBOX, DROPDOWN_ROLL_UP) == 1) direction = 1; // Up + + // Get substrings items from text (items pointers, lengths and count) + int itemCount = 0; + const char **items = GuiTextSplit(text, ';', &itemCount, NULL); + + Rectangle boundsOpen = bounds; + boundsOpen.height = (itemCount + 1)*(bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + if (direction == 1) boundsOpen.y -= itemCount*(bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)) + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING); + + Rectangle itemBounds = bounds; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && (editMode || !guiLocked) && (itemCount > 1) && !guiControlExclusiveMode) + { + Vector2 mousePoint = GetMousePosition(); + + if (editMode) + { + state = STATE_PRESSED; + + // Check if mouse has been pressed or released outside limits + if (!CheckCollisionPointRec(mousePoint, boundsOpen)) + { + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) result = 1; + } + + // Check if already selected item has been pressed again + if (CheckCollisionPointRec(mousePoint, bounds) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) result = 1; + + // Check focused and selected item + for (int i = 0; i < itemCount; i++) + { + // Update item rectangle y position for next item + if (direction == 0) itemBounds.y += (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + else itemBounds.y -= (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + + if (CheckCollisionPointRec(mousePoint, itemBounds)) + { + itemFocused = i; + if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) + { + itemSelected = i; + result = 1; // Item selected + } + break; + } + } + + itemBounds = bounds; + } + else + { + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) + { + result = 1; + state = STATE_PRESSED; + } + else state = STATE_FOCUSED; + } + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + if (editMode) GuiPanel(boundsOpen, NULL); + + // Main (closed) control: draw rounded background + rounded border + { + int borderWidth = GuiGetStyle(DROPDOWNBOX, BORDER_WIDTH); + Color borderColor = GetColor(GuiGetStyle(DROPDOWNBOX, BORDER + state*3)); + Color baseColor = GetColor(GuiGetStyle(DROPDOWNBOX, BASE + state*3)); + + // Filled rounded rect + DrawRectangleRounded(bounds, roundness, segments, baseColor); + } + + GuiDrawText(items[itemSelected], GetTextBounds(DROPDOWNBOX, bounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + state*3))); + + if (editMode) + { + // Draw visible items + for (int i = 0; i < itemCount; i++) + { + // Update item rectangle y position for next item + if (direction == 0) itemBounds.y += (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + else itemBounds.y -= (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + + bool isLastVisible = (i == itemCount - 1); + + if (i == itemSelected) + { + Color borderColor = GetColor(GuiGetStyle(DROPDOWNBOX, BORDER_COLOR_PRESSED)); + Color baseColor = GetColor(GuiGetStyle(DROPDOWNBOX, BASE_COLOR_PRESSED)); + int borderWidth = GuiGetStyle(DROPDOWNBOX, BORDER_WIDTH); + + if (isLastVisible) + { + DrawRectangleRounded(itemBounds, roundness, segments, baseColor); + } + else + { + DrawRectangle(itemBounds.x, itemBounds.y, itemBounds.width, itemBounds.height, baseColor); + if (borderWidth > 0) DrawRectangleLinesEx(itemBounds, borderWidth, borderColor); + } + + GuiDrawText(items[i], GetTextBounds(DROPDOWNBOX, itemBounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT_COLOR_PRESSED))); + } + else if (i == itemFocused) + { + Color borderColor = GetColor(GuiGetStyle(DROPDOWNBOX, BORDER_COLOR_FOCUSED)); + Color baseColor = GetColor(GuiGetStyle(DROPDOWNBOX, BASE_COLOR_FOCUSED)); + int borderWidth = GuiGetStyle(DROPDOWNBOX, BORDER_WIDTH); + + if (isLastVisible) + { + DrawRectangleRounded(itemBounds, roundness, segments, baseColor); + } + else + { + DrawRectangle(itemBounds.x, itemBounds.y, itemBounds.width, itemBounds.height, baseColor); + if (borderWidth > 0) DrawRectangleLinesEx(itemBounds, borderWidth, borderColor); + } + + GuiDrawText(items[i], GetTextBounds(DROPDOWNBOX, itemBounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT_COLOR_FOCUSED))); + } + else + { + // Normal item: draw only text (background left as panel background), + // but if you want a visible background for normal items, uncomment the following: + // DrawRectangle(itemBounds.x, itemBounds.y, itemBounds.width, itemBounds.height, GetColor(GuiGetStyle(DROPDOWNBOX, BASE_COLOR_NORMAL))); + GuiDrawText(items[i], GetTextBounds(DROPDOWNBOX, itemBounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT_COLOR_NORMAL))); + } + } + } + + if (!GuiGetStyle(DROPDOWNBOX, DROPDOWN_ARROW_HIDDEN)) + { + // Draw arrows (using icon if available) +#if defined(RAYGUI_NO_ICONS) + GuiDrawText("v", RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - GuiGetStyle(DROPDOWNBOX, ARROW_PADDING), bounds.y + bounds.height/2 - 2, 10, 10 }, + TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3)))); +#else + GuiDrawText(direction? "#121#" : "#120#", RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - GuiGetStyle(DROPDOWNBOX, ARROW_PADDING), bounds.y + bounds.height/2 - 6, 10, 10 }, + TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3)))); // ICON_ARROW_DOWN_FILL +#endif + } + //-------------------------------------------------------------------- + + *active = itemSelected; + + // TODO: Use result to return more internal states: mouse-press out-of-bounds, mouse-press over selected-item... + return result; // Mouse click: result = 1 +} + // Text Box control // NOTE: Returns true on ENTER pressed (useful for data validation) int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode) @@ -2944,14 +3162,10 @@ // Draw control //-------------------------------------------------------------------- if (state == STATE_PRESSED) - { - GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED))); - } + GuiDrawRectangle(bounds, 0, GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), BLANK); else if (state == STATE_DISABLED) - { - GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_DISABLED))); - } - else GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), BLANK); + GuiDrawRectangle(bounds, 0, GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_DISABLED))); + else GuiDrawRectangle(bounds, 0, GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), BLANK); // Draw text considering index offset if required // NOTE: Text index offset depends on cursor position @@ -6066,7 +6280,7 @@ return result; // Button pressed: result = 1 } -int JUNE_GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode) +JUNE_TextBoxResult JUNE_GuiTextBoxEx(Rectangle bounds, char *text, int textSize, bool editMode) { #if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN) #define RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN 20 // Frames to wait for autocursor movement @@ -6074,122 +6288,255 @@ #if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) #define RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY 1 // Frames delay for autocursor movement #endif - - int result = 0; + #if !defined(JUNE_MAX_VISUAL_LINES) + #define JUNE_MAX_VISUAL_LINES 256 + #endif + + JUNE_TextBoxResult resultStruct = { 0, -1, -1 }; GuiState state = guiState; - bool multiline = true; // TODO: Consider multiline text input - int wrapMode = GuiGetStyle(DEFAULT, TEXT_WRAP_MODE); - Rectangle textBounds = GetTextBounds(TEXTBOX, bounds); - int textLength = (text != NULL)? (int)strlen(text) : 0; // Get current text length + int textLength = (text != NULL) ? (int)strlen(text) : 0; int thisCursorIndex = textBoxCursorIndex; if (thisCursorIndex > textLength) thisCursorIndex = textLength; - // Calculate cursor position for multiline - int cursorLine = 0; // Current line number (0-based) - int lineStart = 0; // Start index of current line - - if (multiline) + // Line height for multiline + float lineHeight = GuiGetStyle(DEFAULT, TEXT_LINE_SPACING); + float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE) / (float)guiFont.baseSize; + float maxLineWidth = textBounds.width - 4; // Small margin + + // Visual line structure: stores start index of each visual line + int visualLineStarts[JUNE_MAX_VISUAL_LINES]; + int visualLineCount = 0; + + // Calculate visual lines (accounting for word wrap) + if (textLength > 0 && maxLineWidth > 0) { - for (int i = 0; i < thisCursorIndex; i++) + int lineStartIdx = 0; + visualLineStarts[visualLineCount++] = 0; + + int idx = 0; + while (idx < textLength && visualLineCount < JUNE_MAX_VISUAL_LINES) { - if (text[i] == '\n') + // Check for hard newline + if (text[idx] == '\n') + { + idx++; + if (idx < textLength && visualLineCount < JUNE_MAX_VISUAL_LINES) + { + visualLineStarts[visualLineCount++] = idx; + lineStartIdx = idx; + } + continue; + } + + // Calculate width from lineStartIdx to current position + float currentWidth = 0; + int lastSpaceIdx = -1; + int charIdx = lineStartIdx; + + while (charIdx < textLength && text[charIdx] != '\n') { - cursorLine++; - lineStart = i + 1; + int cpSize = 0; + int cp = GetCodepointNext(&text[charIdx], &cpSize); + int glyphIdx = GetGlyphIndex(guiFont, cp); + + float glyphWidth; + if (guiFont.glyphs[glyphIdx].advanceX == 0) + glyphWidth = (float)guiFont.recs[glyphIdx].width * scaleFactor; + else + glyphWidth = (float)guiFont.glyphs[glyphIdx].advanceX * scaleFactor; + + if (text[charIdx] == ' ') lastSpaceIdx = charIdx; + + if (currentWidth + glyphWidth > maxLineWidth && charIdx > lineStartIdx) + { + // Need to wrap + int wrapIdx; + if (lastSpaceIdx > lineStartIdx) + { + // Wrap at last space + wrapIdx = lastSpaceIdx + 1; + } + else + { + // No space found, wrap at current char + wrapIdx = charIdx; + } + + if (visualLineCount < JUNE_MAX_VISUAL_LINES) + { + visualLineStarts[visualLineCount++] = wrapIdx; + lineStartIdx = wrapIdx; + idx = wrapIdx; + lastSpaceIdx = -1; + } + break; + } + + currentWidth += glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); + charIdx += cpSize; + } + + if (charIdx >= textLength || text[charIdx] == '\n') + { + idx = charIdx; } } } - - // Calculate horizontal position within current line - char lineText[1024] = { 0 }; - int lineTextLen = 0; - if (multiline) + else { - // Extract current line text up to cursor - int i = lineStart; - while (i < thisCursorIndex && text[i] != '\n' && lineTextLen < 1023) + visualLineStarts[visualLineCount++] = 0; + } + + // Helper: Find visual line for a given text index + int cursorVisualLine = 0; + for (int vl = visualLineCount - 1; vl >= 0; vl--) + { + if (thisCursorIndex >= visualLineStarts[vl]) { - lineText[lineTextLen++] = text[i++]; + cursorVisualLine = vl; + break; } - lineText[lineTextLen] = '\0'; + } + + // Helper: Get end of visual line (exclusive) + int cursorLineStart = visualLineStarts[cursorVisualLine]; + int cursorLineEnd = (cursorVisualLine + 1 < visualLineCount) ? visualLineStarts[cursorVisualLine + 1] : textLength; + // Adjust for newline at end + if (cursorLineEnd > 0 && cursorLineEnd <= textLength && cursorLineEnd > cursorLineStart) + { + if (text[cursorLineEnd - 1] == '\n') cursorLineEnd--; } - int textWidth = multiline ? GuiGetTextWidth(lineText) : (GuiGetTextWidth(text) - GuiGetTextWidth(text + thisCursorIndex)); - int textIndexOffset = 0; // Text index offset to start drawing in the box - - // Line height for multiline (matches GuiDrawText line spacing) - int lineHeight = GuiGetStyle(DEFAULT, TEXT_SIZE); + // Calculate cursor X position within visual line + float cursorXOffset = 0; + for (int k = cursorLineStart; k < thisCursorIndex && k < textLength; k++) + { + if (text[k] == '\n') break; + int cpSize = 0; + int cp = GetCodepointNext(&text[k], &cpSize); + int glyphIdx = GetGlyphIndex(guiFont, cp); + float glyphWidth; + if (guiFont.glyphs[glyphIdx].advanceX == 0) + glyphWidth = (float)guiFont.recs[glyphIdx].width * scaleFactor; + else + glyphWidth = (float)guiFont.glyphs[glyphIdx].advanceX * scaleFactor; + cursorXOffset += glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); + } // Cursor rectangle - // NOTE: Position X and Y values updated for multiline support Rectangle cursor = { - textBounds.x + textWidth + GuiGetStyle(DEFAULT, TEXT_SPACING), - multiline ? (textBounds.y + cursorLine * lineHeight) : (textBounds.y + textBounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)), + textBounds.x + cursorXOffset, + textBounds.y + cursorVisualLine * lineHeight, 2, (float)GuiGetStyle(DEFAULT, TEXT_SIZE) }; - if (cursor.height >= bounds.height) cursor.height = bounds.height - GuiGetStyle(TEXTBOX, BORDER_WIDTH)*2; + if (cursor.height >= bounds.height) cursor.height = bounds.height - GuiGetStyle(TEXTBOX, BORDER_WIDTH) * 2; if (cursor.y < (bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH))) cursor.y = bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH); - // Mouse cursor rectangle - // NOTE: Initialized outside of screen - Rectangle mouseCursor = cursor; - mouseCursor.x = -1; - mouseCursor.width = 1; - - // Blink-cursor frame counter - //if (!autoCursorMode) blinkCursorFrameCounter++; - //else blinkCursorFrameCounter = 0; - // Update control - //-------------------------------------------------------------------- - // WARNING: Text editing is only supported under certain conditions: - if ((state != STATE_DISABLED) && // Control not disabled - !GuiGetStyle(TEXTBOX, TEXT_READONLY) && // TextBox not on read-only mode - !guiLocked && // Gui not locked - !guiControlExclusiveMode && // No gui slider on dragging - (wrapMode == TEXT_WRAP_NONE)) // No wrap mode + if ((state != STATE_DISABLED) && + !GuiGetStyle(TEXTBOX, TEXT_READONLY) && + !guiLocked && + !guiControlExclusiveMode) { Vector2 mousePosition = GetMousePosition(); 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); + bool shiftDown = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); state = STATE_PRESSED; if (textBoxCursorIndex > textLength) textBoxCursorIndex = textLength; - // If text does not fit in the textbox and current cursor position is out of bounds, - // we add an index offset to text for drawing only what requires depending on cursor - while (textWidth >= textBounds.width) - { - int nextCodepointSize = 0; - GetCodepointNext(text + textIndexOffset, &nextCodepointSize); - - textIndexOffset += nextCodepointSize; - - textWidth = GuiGetTextWidth(text + textIndexOffset) - GuiGetTextWidth(text + textBoxCursorIndex); - } - int codepoint = GetCharPressed(); // Get Unicode codepoint - if (multiline && IsKeyPressed(KEY_ENTER)) codepoint = (int)'\n'; + if (IsKeyPressed(KEY_ENTER)) + codepoint = (int)'\n'; // Encode codepoint as UTF-8 int codepointSize = 0; const char *charEncoded = CodepointToUTF8(codepoint, &codepointSize); - // Handle text paste action - if (IsKeyPressed(KEY_V) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL))) + // Helper macro to check if there's an active selection + #define HAS_SELECTION() (textBoxSelectionStart >= 0 && textBoxSelectionEnd >= 0 && textBoxSelectionStart != textBoxSelectionEnd) + #define SELECTION_MIN() ((textBoxSelectionStart < textBoxSelectionEnd) ? textBoxSelectionStart : textBoxSelectionEnd) + #define SELECTION_MAX() ((textBoxSelectionStart > textBoxSelectionEnd) ? textBoxSelectionStart : textBoxSelectionEnd) + + // Ctrl+A: Select all + if (IsKeyPressed(KEY_A) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL) || IsKeyDown(KEY_LEFT_SUPER))) + { + textBoxSelectionStart = 0; + textBoxSelectionEnd = textLength; + textBoxCursorIndex = textLength; + } + // Ctrl+C: Copy selection to clipboard + else if (IsKeyPressed(KEY_C) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL) || IsKeyDown(KEY_LEFT_SUPER))) + { + if (HAS_SELECTION()) + { + int selMin = SELECTION_MIN(); + int selMax = SELECTION_MAX(); + int selLen = selMax - selMin; + char *clipText = (char *)RL_MALLOC(selLen + 1); + if (clipText) + { + memcpy(clipText, text + selMin, selLen); + clipText[selLen] = '\0'; + SetClipboardText(clipText); + RL_FREE(clipText); + } + } + } + // Ctrl+X: Cut selection to clipboard + else if (IsKeyPressed(KEY_X) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL) || IsKeyDown(KEY_LEFT_SUPER))) { + if (HAS_SELECTION()) + { + int selMin = SELECTION_MIN(); + int selMax = SELECTION_MAX(); + int selLen = selMax - selMin; + + // Copy to clipboard + char *clipText = (char *)RL_MALLOC(selLen + 1); + if (clipText) + { + memcpy(clipText, text + selMin, selLen); + clipText[selLen] = '\0'; + SetClipboardText(clipText); + RL_FREE(clipText); + } + + // Delete selection + for (int j = selMax; j <= textLength; j++) text[j - selLen] = text[j]; + textLength -= selLen; + textBoxCursorIndex = selMin; + textBoxSelectionStart = -1; + textBoxSelectionEnd = -1; + } + } + // Ctrl+V: Paste (delete selection first if any) + else if (IsKeyPressed(KEY_V) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL) || IsKeyDown(KEY_LEFT_SUPER))) + { + // Delete selection first if any + if (HAS_SELECTION()) + { + int selMin = SELECTION_MIN(); + int selMax = SELECTION_MAX(); + int selLen = selMax - selMin; + for (int j = selMax; j <= textLength; j++) text[j - selLen] = text[j]; + textLength -= selLen; + textBoxCursorIndex = selMin; + textBoxSelectionStart = -1; + textBoxSelectionEnd = -1; + } + const char *pasteText = GetClipboardText(); if (pasteText != NULL) { @@ -6197,22 +6544,18 @@ int pasteCodepoint; int pasteCodepointSize; - // Count how many codepoints to copy, stopping at the first unwanted control character while (true) { pasteCodepoint = GetCodepointNext(pasteText + pasteLength, &pasteCodepointSize); if (textLength + pasteLength + pasteCodepointSize >= textSize) break; - if (!(multiline && (pasteCodepoint == (int)'\n')) && !(pasteCodepoint >= 32)) break; + if (!((pasteCodepoint == (int)'\n')) && !(pasteCodepoint >= 32)) break; pasteLength += pasteCodepointSize; } if (pasteLength > 0) { - // Move forward data from cursor position - for (int i = textLength + pasteLength; i > textBoxCursorIndex; i--) text[i] = text[i - pasteLength]; - - // Paste data in at cursor - for (int i = 0; i < pasteLength; i++) text[textBoxCursorIndex + i] = pasteText[i]; + for (int j = textLength + pasteLength; j > textBoxCursorIndex; j--) text[j] = text[j - pasteLength]; + for (int j = 0; j < pasteLength; j++) text[textBoxCursorIndex + j] = pasteText[j]; textBoxCursorIndex += pasteLength; textLength += pasteLength; @@ -6220,39 +6563,76 @@ } } } - else if (((multiline && (codepoint == (int)'\n')) || (codepoint >= 32)) && ((textLength + codepointSize) < textSize)) + else if ((((codepoint == (int)'\n')) || (codepoint >= 32)) && ((textLength + codepointSize) < textSize)) { + // Delete selection first if any + if (HAS_SELECTION()) + { + int selMin = SELECTION_MIN(); + int selMax = SELECTION_MAX(); + int selLen = selMax - selMin; + for (int j = selMax; j <= textLength; j++) text[j - selLen] = text[j]; + textLength -= selLen; + textBoxCursorIndex = selMin; + textBoxSelectionStart = -1; + textBoxSelectionEnd = -1; + } + // Adding codepoint to text, at current cursor position - - // Move forward data from cursor position - for (int i = (textLength + codepointSize); i > textBoxCursorIndex; i--) text[i] = text[i - codepointSize]; - - // Add new codepoint in current cursor position - for (int i = 0; i < codepointSize; i++) text[textBoxCursorIndex + i] = charEncoded[i]; - - textBoxCursorIndex += codepointSize; - textLength += codepointSize; - - // Make sure text last character is EOL - text[textLength] = '\0'; + if ((textLength + codepointSize) < textSize) + { + for (int j = (textLength + codepointSize); j > textBoxCursorIndex; j--) text[j] = text[j - codepointSize]; + for (int j = 0; j < codepointSize; j++) text[textBoxCursorIndex + j] = charEncoded[j]; + + textBoxCursorIndex += codepointSize; + textLength += codepointSize; + text[textLength] = '\0'; + } + } + + #undef HAS_SELECTION + #undef SELECTION_MIN + #undef SELECTION_MAX + + // Move cursor to start (with Shift selection support) + if ((textLength > 0) && IsKeyPressed(KEY_HOME)) + { + if (shiftDown && textBoxSelectionStart < 0) textBoxSelectionStart = textBoxCursorIndex; + textBoxCursorIndex = 0; + if (shiftDown) textBoxSelectionEnd = textBoxCursorIndex; + else { textBoxSelectionStart = -1; textBoxSelectionEnd = -1; } } - // Move cursor to start - if ((textLength > 0) && IsKeyPressed(KEY_HOME)) textBoxCursorIndex = 0; - - // Move cursor to end - if ((textLength > textBoxCursorIndex) && IsKeyPressed(KEY_END)) textBoxCursorIndex = textLength; - - // Delete related codepoints from text, after current cursor position - if ((textLength > textBoxCursorIndex) && IsKeyPressed(KEY_DELETE) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL))) + // Move cursor to end (with Shift selection support) + if ((textLength > textBoxCursorIndex) && IsKeyPressed(KEY_END)) + { + if (shiftDown && textBoxSelectionStart < 0) textBoxSelectionStart = textBoxCursorIndex; + textBoxCursorIndex = textLength; + if (shiftDown) textBoxSelectionEnd = textBoxCursorIndex; + else { textBoxSelectionStart = -1; textBoxSelectionEnd = -1; } + } + + // Delete selection if any (on Delete or Backspace) + if ((textBoxSelectionStart >= 0 && textBoxSelectionEnd >= 0 && textBoxSelectionStart != textBoxSelectionEnd) && + (IsKeyPressed(KEY_DELETE) || IsKeyPressed(KEY_BACKSPACE))) + { + int selMin = (textBoxSelectionStart < textBoxSelectionEnd) ? textBoxSelectionStart : textBoxSelectionEnd; + int selMax = (textBoxSelectionStart > textBoxSelectionEnd) ? textBoxSelectionStart : textBoxSelectionEnd; + int selLen = selMax - selMin; + for (int j = selMax; j <= textLength; j++) text[j - selLen] = text[j]; + textLength -= selLen; + textBoxCursorIndex = selMin; + textBoxSelectionStart = -1; + textBoxSelectionEnd = -1; + } + // Ctrl+Delete: Delete word after cursor + else if ((textLength > textBoxCursorIndex) && IsKeyPressed(KEY_DELETE) && (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 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) @@ -6264,329 +6644,366 @@ nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize); } - // 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 character after cursor else if ((textLength > textBoxCursorIndex) && (IsKeyPressed(KEY_DELETE) || (IsKeyDown(KEY_DELETE) && autoCursorShouldTrigger))) { - // Delete single codepoint from text, after current cursor position - 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 - if ((textBoxCursorIndex > 0) && IsKeyPressed(KEY_BACKSPACE) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL))) + // Ctrl+Backspace: Delete word before cursor + else if ((textBoxCursorIndex > 0) && IsKeyPressed(KEY_BACKSPACE) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL))) { int offset = textBoxCursorIndex; int accCodepointSize = 0; int prevCodepointSize = 0; int prevCodepoint = 0; - // Check whitespace to delete (ASCII only) while (offset > 0) { prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize); if (!isspace(prevCodepoint & 0xff)) 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; } - // Move text after cursor forward (including final null terminator) for (int i = textBoxCursorIndex; i <= textLength; i++) text[i - accCodepointSize] = text[i]; - textLength -= accCodepointSize; textBoxCursorIndex -= accCodepointSize; } - + // Backspace single character before cursor else if ((textBoxCursorIndex > 0) && (IsKeyPressed(KEY_BACKSPACE) || (IsKeyDown(KEY_BACKSPACE) && autoCursorShouldTrigger))) { - // Delete single codepoint from text, before current cursor position - int prevCodepointSize = 0; - GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize); - - // Move text after cursor forward (including final null terminator) for (int i = textBoxCursorIndex; i <= textLength; i++) text[i - prevCodepointSize] = text[i]; - textLength -= prevCodepointSize; textBoxCursorIndex -= prevCodepointSize; } - // Move cursor position with keys + // Move cursor position with keys (with Shift selection support) if ((textBoxCursorIndex > 0) && IsKeyPressed(KEY_LEFT) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL))) { + if (shiftDown && textBoxSelectionStart < 0) textBoxSelectionStart = textBoxCursorIndex; + int offset = textBoxCursorIndex; - //int accCodepointSize = 0; int prevCodepointSize = 0; int prevCodepoint = 0; - // Check whitespace to skip (ASCII only) while (offset > 0) { prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize); if (!isspace(prevCodepoint & 0xff)) break; - 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; + + if (shiftDown) textBoxSelectionEnd = textBoxCursorIndex; + else { textBoxSelectionStart = -1; textBoxSelectionEnd = -1; } } else if ((textBoxCursorIndex > 0) && (IsKeyPressed(KEY_LEFT) || (IsKeyDown(KEY_LEFT) && autoCursorShouldTrigger))) { + if (shiftDown && textBoxSelectionStart < 0) textBoxSelectionStart = textBoxCursorIndex; + int prevCodepointSize = 0; GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize); - textBoxCursorIndex -= prevCodepointSize; + + if (shiftDown) textBoxSelectionEnd = textBoxCursorIndex; + else { textBoxSelectionStart = -1; textBoxSelectionEnd = -1; } } else if ((textLength > textBoxCursorIndex) && IsKeyPressed(KEY_RIGHT) && (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL))) { + if (shiftDown && textBoxSelectionStart < 0) textBoxSelectionStart = textBoxCursorIndex; + 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) { if ((puctuation && !ispunct(nextCodepoint & 0xff)) || (!puctuation && (isspace(nextCodepoint & 0xff) || ispunct(nextCodepoint & 0xff)))) break; - offset += 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; + + if (shiftDown) textBoxSelectionEnd = textBoxCursorIndex; + else { textBoxSelectionStart = -1; textBoxSelectionEnd = -1; } } else if ((textLength > textBoxCursorIndex) && (IsKeyPressed(KEY_RIGHT) || (IsKeyDown(KEY_RIGHT) && autoCursorShouldTrigger))) { + if (shiftDown && textBoxSelectionStart < 0) textBoxSelectionStart = textBoxCursorIndex; + int nextCodepointSize = 0; GetCodepointNext(text + textBoxCursorIndex, &nextCodepointSize); - textBoxCursorIndex += nextCodepointSize; + + if (shiftDown) textBoxSelectionEnd = textBoxCursorIndex; + else { textBoxSelectionStart = -1; textBoxSelectionEnd = -1; } } - // Vertical cursor movement for multiline - if (multiline && (IsKeyPressed(KEY_UP) || (IsKeyDown(KEY_UP) && autoCursorShouldTrigger))) + // Vertical cursor movement using visual lines (with Shift selection support) + if ((IsKeyPressed(KEY_UP) || (IsKeyDown(KEY_UP) && autoCursorShouldTrigger))) { - // Find start of current line - int currentLineStart = textBoxCursorIndex; - while (currentLineStart > 0 && text[currentLineStart - 1] != '\n') currentLineStart--; - - // Calculate horizontal position in current line - int horizontalPos = textBoxCursorIndex - currentLineStart; - - // Find start of previous line - if (currentLineStart > 0) + if (shiftDown && textBoxSelectionStart < 0) textBoxSelectionStart = textBoxCursorIndex; + + // Find current visual line + int currVisLine = 0; + for (int vl = visualLineCount - 1; vl >= 0; vl--) + { + if (textBoxCursorIndex >= visualLineStarts[vl]) { currVisLine = vl; break; } + } + + // Calculate X offset in current line + int currLineStart = visualLineStarts[currVisLine]; + float xOffset = 0; + for (int k = currLineStart; k < textBoxCursorIndex && k < textLength; k++) { - int prevLineEnd = currentLineStart - 1; // Skip the newline - int prevLineStart = prevLineEnd; - while (prevLineStart > 0 && text[prevLineStart - 1] != '\n') prevLineStart--; - - // Move to same horizontal position on previous line (or end of line if shorter) - int prevLineLength = prevLineEnd - prevLineStart; - int targetPos = (horizontalPos < prevLineLength) ? horizontalPos : prevLineLength; - textBoxCursorIndex = prevLineStart + targetPos; + if (text[k] == '\n') break; + int cpSize = 0; + int cp = GetCodepointNext(&text[k], &cpSize); + int gi = GetGlyphIndex(guiFont, cp); + float gw = (guiFont.glyphs[gi].advanceX == 0) ? (float)guiFont.recs[gi].width * scaleFactor : (float)guiFont.glyphs[gi].advanceX * scaleFactor; + xOffset += gw + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); + } + + if (currVisLine > 0) + { + // Move to previous visual line at same X position + int prevLineStart = visualLineStarts[currVisLine - 1]; + int prevLineEnd = visualLineStarts[currVisLine]; + if (prevLineEnd > 0 && text[prevLineEnd - 1] == '\n') prevLineEnd--; + + float accum = 0; + textBoxCursorIndex = prevLineStart; + for (int k = prevLineStart; k < prevLineEnd; ) + { + int cpSize = 0; + int cp = GetCodepointNext(&text[k], &cpSize); + int gi = GetGlyphIndex(guiFont, cp); + float gw = (guiFont.glyphs[gi].advanceX == 0) ? (float)guiFont.recs[gi].width * scaleFactor : (float)guiFont.glyphs[gi].advanceX * scaleFactor; + if (accum + gw / 2 >= xOffset) break; + accum += gw + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); + textBoxCursorIndex = k + cpSize; + k += cpSize; + } } else { - // Already on first line, move to start textBoxCursorIndex = 0; } + + if (shiftDown) textBoxSelectionEnd = textBoxCursorIndex; + else { textBoxSelectionStart = -1; textBoxSelectionEnd = -1; } } - else if (multiline && (IsKeyPressed(KEY_DOWN) || (IsKeyDown(KEY_DOWN) && autoCursorShouldTrigger))) + else if ((IsKeyPressed(KEY_DOWN) || (IsKeyDown(KEY_DOWN) && autoCursorShouldTrigger))) { - // Find start of current line - int currentLineStart = textBoxCursorIndex; - while (currentLineStart > 0 && text[currentLineStart - 1] != '\n') currentLineStart--; - - // Calculate horizontal position in current line - int horizontalPos = textBoxCursorIndex - currentLineStart; - - // Find end of current line - int currentLineEnd = textBoxCursorIndex; - while (currentLineEnd < textLength && text[currentLineEnd] != '\n') currentLineEnd++; - - // Find next line - if (currentLineEnd < textLength) + if (shiftDown && textBoxSelectionStart < 0) textBoxSelectionStart = textBoxCursorIndex; + + // Find current visual line + int currVisLine = 0; + for (int vl = visualLineCount - 1; vl >= 0; vl--) + { + if (textBoxCursorIndex >= visualLineStarts[vl]) { currVisLine = vl; break; } + } + + // Calculate X offset in current line + int currLineStart = visualLineStarts[currVisLine]; + float xOffset = 0; + for (int k = currLineStart; k < textBoxCursorIndex && k < textLength; k++) { - int nextLineStart = currentLineEnd + 1; // Skip the newline - int nextLineEnd = nextLineStart; - while (nextLineEnd < textLength && text[nextLineEnd] != '\n') nextLineEnd++; - - // Move to same horizontal position on next line (or end of line if shorter) - int nextLineLength = nextLineEnd - nextLineStart; - int targetPos = (horizontalPos < nextLineLength) ? horizontalPos : nextLineLength; - textBoxCursorIndex = nextLineStart + targetPos; + if (text[k] == '\n') break; + int cpSize = 0; + int cp = GetCodepointNext(&text[k], &cpSize); + int gi = GetGlyphIndex(guiFont, cp); + float gw = (guiFont.glyphs[gi].advanceX == 0) ? (float)guiFont.recs[gi].width * scaleFactor : (float)guiFont.glyphs[gi].advanceX * scaleFactor; + xOffset += gw + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); + } + + if (currVisLine < visualLineCount - 1) + { + // Move to next visual line at same X position + int nextLineStart = visualLineStarts[currVisLine + 1]; + int nextLineEnd = (currVisLine + 2 < visualLineCount) ? visualLineStarts[currVisLine + 2] : textLength; + if (nextLineEnd > nextLineStart && text[nextLineEnd - 1] == '\n') nextLineEnd--; + + float accum = 0; + textBoxCursorIndex = nextLineStart; + for (int k = nextLineStart; k < nextLineEnd; ) + { + int cpSize = 0; + int cp = GetCodepointNext(&text[k], &cpSize); + int gi = GetGlyphIndex(guiFont, cp); + float gw = (guiFont.glyphs[gi].advanceX == 0) ? (float)guiFont.recs[gi].width * scaleFactor : (float)guiFont.glyphs[gi].advanceX * scaleFactor; + if (accum + gw / 2 >= xOffset) break; + accum += gw + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); + textBoxCursorIndex = k + cpSize; + k += cpSize; + } } else { - // Already on last line, move to end textBoxCursorIndex = textLength; } + + if (shiftDown) textBoxSelectionEnd = textBoxCursorIndex; + else { textBoxSelectionStart = -1; textBoxSelectionEnd = -1; } } - // Move cursor position with mouse - if (CheckCollisionPointRec(mousePosition, textBounds)) // Mouse hover text + // Move cursor position with mouse using visual lines { - float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/(float)guiFont.baseSize; - int codepointIndex = 0; - float glyphWidth = 0.0f; - float widthToMouseX = 0; int mouseCursorIndex = 0; - - for (int i = textIndexOffset; i < textLength; i += codepointSize) + bool mouseInBounds = CheckCollisionPointRec(mousePosition, textBounds); + bool shouldCalculateMousePos = mouseInBounds || textBoxSelecting; + + if (shouldCalculateMousePos && textLength > 0) { - codepoint = GetCodepointNext(&text[i], &codepointSize); - codepointIndex = GetGlyphIndex(guiFont, codepoint); - - if (guiFont.glyphs[codepointIndex].advanceX == 0) glyphWidth = ((float)guiFont.recs[codepointIndex].width*scaleFactor); - else glyphWidth = ((float)guiFont.glyphs[codepointIndex].advanceX*scaleFactor); - - if (mousePosition.x <= (textBounds.x + (widthToMouseX + glyphWidth/2))) - { - mouseCursor.x = textBounds.x + widthToMouseX; - mouseCursorIndex = i; - printf("before: %i\n", mouseCursorIndex); - break; - } - - if (mousePosition.y >= textBounds.y && mousePosition.y <= textBounds.height) + // Determine which visual line the mouse is on + int mouseLine = (int)((mousePosition.y - textBounds.y) / lineHeight); + if (mouseLine < 0) mouseLine = 0; + if (mouseLine >= visualLineCount) mouseLine = visualLineCount - 1; + + // Get the start and end of the target visual line + int targetLineStart = visualLineStarts[mouseLine]; + int targetLineEnd = (mouseLine + 1 < visualLineCount) ? visualLineStarts[mouseLine + 1] : textLength; + if (targetLineEnd > targetLineStart && text[targetLineEnd - 1] == '\n') targetLineEnd--; + + // Find character position within the line based on mouse X + float relativeMouseX = mousePosition.x - textBounds.x; + if (relativeMouseX < 0) relativeMouseX = 0; + + float widthAccum = 0; + mouseCursorIndex = targetLineStart; + + for (int k = targetLineStart; k < targetLineEnd; ) { - mouseCursor.y = mousePosition.y; - mouseCursorIndex = i; - int number_of_n = (int)(mousePosition.y / lineHeight); - for (int i = 0; i < textSize; i++) + if (text[k] == '\n') break; + int cpSize = 0; + int cp = GetCodepointNext(&text[k], &cpSize); + int gi = GetGlyphIndex(guiFont, cp); + + float gw; + if (guiFont.glyphs[gi].advanceX == 0) + gw = (float)guiFont.recs[gi].width * scaleFactor; + else + gw = (float)guiFont.glyphs[gi].advanceX * scaleFactor; + + float charMidpoint = widthAccum + gw / 2.0f; + + if (relativeMouseX <= charMidpoint) { - if (text[i] == '\n') - { - number_of_n--; - if (number_of_n == 0) - mouseCursorIndex += i; - } + mouseCursorIndex = k; + break; } - break; + + widthAccum += gw + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); + mouseCursorIndex = k + cpSize; + k += cpSize; } - widthToMouseX += (glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); + // Clamp to line end + if (mouseCursorIndex > targetLineEnd) mouseCursorIndex = targetLineEnd; } - - // Check if mouse cursor is at the last position - int textEndWidth = GuiGetTextWidth(text + textIndexOffset); - if (GetMousePosition().x >= (textBounds.x + textEndWidth - glyphWidth/2)) + else if (shouldCalculateMousePos) { - mouseCursor.x = textBounds.x + textEndWidth; - mouseCursorIndex = textLength; + mouseCursorIndex = 0; } - // Place cursor at required index on mouse click - if ((mouseCursor.x >= 0) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) + // Mouse selection: start selection on mouse press (only when in bounds) + if (mouseInBounds && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { - cursor.x = mouseCursor.x; + textBoxCursorIndex = mouseCursorIndex; + textBoxSelectionStart = mouseCursorIndex; + textBoxSelectionEnd = mouseCursorIndex; + textBoxSelecting = true; + } + // Mouse selection: update selection while dragging (even outside bounds) + else if (textBoxSelecting && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) + { + textBoxSelectionEnd = mouseCursorIndex; textBoxCursorIndex = mouseCursorIndex; } } - else mouseCursor.x = -1; - - // Recalculate cursor position for multiline - if (multiline) + + // End mouse selection when button released + if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) + { + textBoxSelecting = false; + } + + // Recalculate cursor position using visual lines + // (Visual lines already calculated at the start - need to recalculate after any changes) + int newCursorVisLine = 0; + for (int vl = visualLineCount - 1; vl >= 0; vl--) { - // Recalculate cursor line and position - int newCursorLine = 0; - int newLineStart = 0; - - for (int i = 0; i < textBoxCursorIndex; i++) - { - if (text[i] == '\n') - { - newCursorLine++; - newLineStart = i + 1; - } - } - - // Extract current line text up to cursor - char currentLineText[1024] = { 0 }; - int currentLineLen = 0; - int i = newLineStart; - while (i < textBoxCursorIndex && text[i] != '\n' && currentLineLen < 1023) - { - currentLineText[currentLineLen++] = text[i++]; - } - currentLineText[currentLineLen] = '\0'; - - cursor.x = bounds.x + GuiGetStyle(TEXTBOX, TEXT_PADDING) + GuiGetTextWidth(currentLineText) + GuiGetStyle(DEFAULT, TEXT_SPACING); - cursor.y = textBounds.y + (newCursorLine * lineHeight * 1.5); + if (textBoxCursorIndex >= visualLineStarts[vl]) { newCursorVisLine = vl; break; } } - else + + int newCursorLineStart = visualLineStarts[newCursorVisLine]; + float newCursorXOffset = 0; + for (int k = newCursorLineStart; k < textBoxCursorIndex && k < textLength; k++) { - cursor.x = bounds.x + GuiGetStyle(TEXTBOX, TEXT_PADDING) + GuiGetTextWidth(text + textIndexOffset) - GuiGetTextWidth(text + textBoxCursorIndex) + GuiGetStyle(DEFAULT, TEXT_SPACING); + if (text[k] == '\n') break; + int cpSize = 0; + int cp = GetCodepointNext(&text[k], &cpSize); + int gi = GetGlyphIndex(guiFont, cp); + float gw = (guiFont.glyphs[gi].advanceX == 0) ? (float)guiFont.recs[gi].width * scaleFactor : (float)guiFont.glyphs[gi].advanceX * scaleFactor; + newCursorXOffset += gw + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); } + cursor.x = textBounds.x + newCursorXOffset; + cursor.y = textBounds.y + newCursorVisLine * lineHeight; + // Finish text editing on ENTER or mouse click outside bounds - if ((!multiline && IsKeyPressed(KEY_ENTER)) || - (!CheckCollisionPointRec(mousePosition, bounds) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON))) + if ((!CheckCollisionPointRec(mousePosition, bounds) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON))) { textBoxCursorIndex = 0; // GLOBAL: Reset the shared cursor index autoCursorCounter = 0; // GLOBAL: Reset counter for repeated keystrokes - result = 1; + textBoxSelectionStart = -1; + textBoxSelectionEnd = -1; + textBoxSelecting = false; + resultStruct.result = 1; } } else @@ -6599,7 +7016,9 @@ { textBoxCursorIndex = textLength; // GLOBAL: Place cursor index to the end of current text autoCursorCounter = 0; // GLOBAL: Reset counter for repeated keystrokes - result = 1; + textBoxSelectionStart = -1; + textBoxSelectionEnd = -1; + resultStruct.result = 1; } } } @@ -6618,16 +7037,83 @@ } else GuiDrawRectangle(bounds, 0, GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED))); - // Draw text considering index offset if required - // NOTE: Text index offset depends on cursor position - // Set vertical alignment to top for multiline + // Draw selection highlight using visual lines + if (editMode && textBoxSelectionStart >= 0 && textBoxSelectionEnd >= 0 && textBoxSelectionStart != textBoxSelectionEnd) + { + int selMin = (textBoxSelectionStart < textBoxSelectionEnd) ? textBoxSelectionStart : textBoxSelectionEnd; + int selMax = (textBoxSelectionStart > textBoxSelectionEnd) ? textBoxSelectionStart : textBoxSelectionEnd; + + // Find visual line for selection start + int selStartVisLine = 0; + for (int vl = visualLineCount - 1; vl >= 0; vl--) + { + if (selMin >= visualLineStarts[vl]) { selStartVisLine = vl; break; } + } + + // Find visual line for selection end + int selEndVisLine = 0; + for (int vl = visualLineCount - 1; vl >= 0; vl--) + { + if (selMax >= visualLineStarts[vl]) { selEndVisLine = vl; break; } + } + + Color selectionColor = (Color){ 100, 150, 255, 100 }; + + // Draw selection for each visual line in range + for (int vl = selStartVisLine; vl <= selEndVisLine; vl++) + { + int vlStart = visualLineStarts[vl]; + int vlEnd = (vl + 1 < visualLineCount) ? visualLineStarts[vl + 1] : textLength; + if (vlEnd > vlStart && vlEnd <= textLength && text[vlEnd - 1] == '\n') vlEnd--; + + // Determine selection bounds within this visual line + int drawStart = (vl == selStartVisLine) ? selMin : vlStart; + int drawEnd = (vl == selEndVisLine) ? selMax : vlEnd; + + // Calculate X positions + float xStart = 0; + for (int k = vlStart; k < drawStart && k < textLength; k++) + { + if (text[k] == '\n') break; + int cpSize = 0; + int cp = GetCodepointNext(&text[k], &cpSize); + int gi = GetGlyphIndex(guiFont, cp); + float gw = (guiFont.glyphs[gi].advanceX == 0) ? (float)guiFont.recs[gi].width * scaleFactor : (float)guiFont.glyphs[gi].advanceX * scaleFactor; + xStart += gw + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); + } + + float xEnd = 0; + for (int k = vlStart; k < drawEnd && k < textLength; k++) + { + if (text[k] == '\n') break; + int cpSize = 0; + int cp = GetCodepointNext(&text[k], &cpSize); + int gi = GetGlyphIndex(guiFont, cp); + float gw = (guiFont.glyphs[gi].advanceX == 0) ? (float)guiFont.recs[gi].width * scaleFactor : (float)guiFont.glyphs[gi].advanceX * scaleFactor; + xEnd += gw + (float)GuiGetStyle(DEFAULT, TEXT_SPACING); + } + + Rectangle selRect = { + textBounds.x + xStart, + textBounds.y + vl * lineHeight, + xEnd - xStart, + (float)GuiGetStyle(DEFAULT, TEXT_SIZE) + }; + GuiDrawRectangle(selRect, 0, BLANK, selectionColor); + } + } + + // Draw text with word wrap enabled int prevVerticalAlignment = GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL); - if (multiline) GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, TEXT_ALIGN_TOP); - - GuiDrawText(text + textIndexOffset, textBounds, GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(TEXTBOX, TEXT + (state*3)))); - - // Restore previous vertical alignment - if (multiline) GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, prevVerticalAlignment); + int prevWrapMode = GuiGetStyle(DEFAULT, TEXT_WRAP_MODE); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, TEXT_ALIGN_TOP); + GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, TEXT_WRAP_CHAR); + + GuiDrawText(text, textBounds, GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(TEXTBOX, TEXT + (state*3)))); + + // Restore previous settings + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, prevVerticalAlignment); + GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, prevWrapMode); // Draw cursor if (editMode && !GuiGetStyle(TEXTBOX, TEXT_READONLY)) @@ -6641,7 +7127,11 @@ else if (state == STATE_FOCUSED) GuiTooltip(bounds); //-------------------------------------------------------------------- - return result; // Mouse button pressed: result = 1 + // Return selection info + resultStruct.selectionStart = textBoxSelectionStart; + resultStruct.selectionEnd = textBoxSelectionEnd; + + return resultStruct; } void JUNE_DrawRectangleLinesNoBottom(Rectangle rect, float thickness, Color color) {