REVIEWED: GuiDrawText(), support word-wrap (read-only text)

This commit is contained in:
Ray 2023-08-28 20:27:02 +02:00
parent da377d736d
commit 1ad1907dc1
2 changed files with 216 additions and 258 deletions

View File

@ -35,6 +35,9 @@
#include "raylib.h"
//#define RAYGUI_DEBUG_RECS_BOUNDS
//#define RAYGUI_DEBUG_TEXT_BOUNDS
#define RAYGUI_IMPLEMENTATION
//#define RAYGUI_CUSTOM_ICONS // It requires providing gui_icons.h in the same directory
//#include "gui_icons.h" // External icons data provided, it can be generated with rGuiIcons tool
@ -56,7 +59,7 @@ int main()
{
// Initialization
//---------------------------------------------------------------------------------------
const int screenWidth = 690;
const int screenWidth = 960;
const int screenHeight = 560;
InitWindow(screenWidth, screenHeight, "raygui - controls test suite");
@ -79,6 +82,9 @@ int main()
char textBoxText[64] = "Text box";
bool textBoxEditMode = false;
char textBoxMultiText[1024] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
bool textBoxMultiEditMode = false;
int listViewScrollIndex = 0;
int listViewActive = -1;
@ -243,7 +249,7 @@ int main()
GuiSlider((Rectangle){ 355, 400, 165, 20 }, "TEST", TextFormat("%2.2f", sliderValue), &sliderValue, -50, 100);
GuiSliderBar((Rectangle){ 320, 430, 200, 20 }, NULL, TextFormat("%i", (int)sliderBarValue), &sliderBarValue, 0, 100);
GuiProgressBar((Rectangle){ 320, 460, 200, 20 }, NULL, TextFormat(" %i%%", (int)(progressValue*100)), &progressValue, 0.0f, 1.0f);
GuiProgressBar((Rectangle){ 320, 460, 200, 20 }, NULL, TextFormat("%i%%", (int)(progressValue*100)), &progressValue, 0.0f, 1.0f);
GuiEnable();
// NOTE: View rectangle could be used to perform some scissor test
@ -251,12 +257,20 @@ int main()
GuiScrollPanel((Rectangle){ 560, 25, 102, 354 }, NULL, (Rectangle){ 560, 25, 300, 1200 }, &viewScroll, &view);
Vector2 mouseCell = { 0 };
GuiGrid((Rectangle) { 560, 25 + 180 + 195, 100, 120 }, NULL, 20, 2, &mouseCell);
GuiGrid((Rectangle) { 560, 25 + 180 + 195, 100, 120 }, NULL, 20, 3, &mouseCell);
GuiStatusBar((Rectangle){ 0, (float)GetScreenHeight() - 20, (float)GetScreenWidth(), 20 }, "This is a status bar");
//GuiStatusBar((Rectangle){ 0, (float)GetScreenHeight() - 20, (float)GetScreenWidth(), 20 }, "This is a status bar");
GuiColorBarAlpha((Rectangle){ 320, 490, 200, 30 }, NULL, &alphaValue);
GuiSetStyle(TEXTBOX, TEXT_ALIGNMENT_VERTICAL, 1); // 0-CENTERED, 1-TOP, 2-BOTTOM (does not work as expected in case of vertical alignment)
GuiSetStyle(TEXTBOX, TEXT_MULTILINE, 1); // 0-OFF (single line), 1-OFF (multiple text lines supported)
GuiSetStyle(TEXTBOX, TEXT_WRAP_MODE, 2); // 0-NO_WRAP, 1-CHAR_WRAP, 2-WORD_WRAP (if wrap mode enabled, text editing is not supported)
if (GuiTextBox((Rectangle){ 678, 25, 258, 492 }, textBoxMultiText, 1024, textBoxMultiEditMode)) textBoxMultiEditMode = !textBoxMultiEditMode;
GuiSetStyle(TEXTBOX, TEXT_WRAP_MODE, 0);
GuiSetStyle(TEXTBOX, TEXT_MULTILINE, 0);
GuiSetStyle(TEXTBOX, TEXT_ALIGNMENT_VERTICAL, 0);
if (showMessageBox)
{
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade(RAYWHITE, 0.8f));
@ -269,7 +283,7 @@ int main()
if (showTextInputBox)
{
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade(RAYWHITE, 0.8f));
int result = GuiTextInputBox((Rectangle){ (float)GetScreenWidth()/2 - 120, (float)GetScreenHeight()/2 - 60, 240, 140 }, "Save", GuiIconText(ICON_FILE_SAVE, "Save file as..."), "Ok;Cancel", textInput, 255, NULL);
int result = GuiTextInputBox((Rectangle){ (float)GetScreenWidth()/2 - 120, (float)GetScreenHeight()/2 - 60, 240, 140 }, GuiIconText(ICON_FILE_SAVE, "Save file as..."), "Introduce output file name:", "Ok;Cancel", textInput, 255, NULL);
if (result == 1)
{

View File

@ -132,6 +132,9 @@
* Includes custom ricons.h header defining a set of custom icons,
* this file can be generated using rGuiIcons tool
*
* #define RAYGUI_DEBUG_RECS_BOUNDS
* Draw control bounds rectangles for debug
*
* #define RAYGUI_DEBUG_TEXT_BOUNDS
* Draw text bounds rectangles for debug
*
@ -1380,7 +1383,7 @@ static Vector3 ConvertRGBtoHSV(Vector3 rgb); // Convert color
static int GuiScrollBar(Rectangle bounds, int value, int minValue, int maxValue); // Scroll bar control, used by GuiScrollPanel()
static void GuiTooltip(Rectangle controlRec); // Draw tooltip using control rec position
static Color GuiFade(Color color, float alpha);
static Color GuiFade(Color color, float alpha); // Fade color by an alpha factor
//----------------------------------------------------------------------------------
// Gui Setup Functions Definition
@ -2330,6 +2333,7 @@ int GuiTextBox(Rectangle bounds, char *text, int bufferSize, bool editMode)
int alignmentVertical = GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT_VERTICAL);
int multiline = GuiGetStyle(TEXTBOX, TEXT_MULTILINE);
int wrapMode = GuiGetStyle(TEXTBOX, TEXT_WRAP_MODE);
// Cursor rectangle
// NOTE: Position X value should be updated
@ -2366,7 +2370,8 @@ int GuiTextBox(Rectangle bounds, char *text, int bufferSize, bool editMode)
// Update control
//--------------------------------------------------------------------
if ((state != STATE_DISABLED) && !guiLocked && !guiSliderDragging && (GuiGetStyle(TEXTBOX, TEXT_READONLY) == 0))
// WARNING: If wrapMode is enabled, text editing is not supported
if ((state != STATE_DISABLED) && !guiLocked && !guiSliderDragging && (GuiGetStyle(TEXTBOX, TEXT_READONLY) == 0) && (wrapMode == 0))
{
Vector2 mousePoint = GetMousePosition();
@ -3725,8 +3730,9 @@ int GuiGrid(Rectangle bounds, const char *text, float spacing, int subdivs, Vect
Vector2 mousePoint = GetMousePosition();
Vector2 currentMouseCell = { 0 };
int linesV = ((int)(bounds.width/spacing))*subdivs + subdivs;
int linesH = ((int)(bounds.height/spacing))*subdivs + subdivs;
float spaceWidth = spacing/(float)subdivs;
int linesV = (int)(bounds.width/spaceWidth) + 1;
int linesH = (int)(bounds.height/spaceWidth) + 1;
// Update control
//--------------------------------------------------------------------
@ -3930,6 +3936,7 @@ void GuiLoadStyleDefault(void)
GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
GuiSetStyle(BUTTON, BORDER_WIDTH, 2);
GuiSetStyle(SLIDER, TEXT_PADDING, 4);
GuiSetStyle(PROGRESSBAR, TEXT_PADDING, 4);
GuiSetStyle(CHECKBOX, TEXT_PADDING, 4);
GuiSetStyle(CHECKBOX, TEXT_ALIGNMENT, TEXT_ALIGN_RIGHT);
GuiSetStyle(TEXTBOX, TEXT_PADDING, 4);
@ -4513,6 +4520,35 @@ const char **GetTextLines(const char *text, int *count)
return lines;
}
// Get text width to next space for provided string
static int GetNextSpaceWidth(const char *text, int nextSpaceIndex)
{
int width = 0;
int codepointByteCount = 0;
int codepoint = 0;
int index = 0;
float glyphWidth = 0;
float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/guiFont.baseSize;
for (int i = 0; text[i] != '\0'; i++)
{
if (text[i] != ' ')
{
codepoint = GetCodepoint(&text[i], &codepointByteCount);
index = GetGlyphIndex(guiFont, codepoint);
glyphWidth = (guiFont.glyphs[index].advanceX == 0)? guiFont.recs[index].width*scaleFactor : guiFont.glyphs[index].advanceX*scaleFactor;
width += (glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING));
}
else
{
nextSpaceIndex = i;
break;
}
}
return width;
}
// Gui draw text using default font
static void GuiDrawText(const char *text, Rectangle bounds, int alignment, Color tint)
{
@ -4522,21 +4558,27 @@ static void GuiDrawText(const char *text, Rectangle bounds, int alignment, Color
#define ICON_TEXT_PADDING 4
#endif
int wrapMode = GuiGetStyle(TEXTBOX, TEXT_WRAP_MODE);
int alignmentVertical = GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT_VERTICAL);
if ((text == NULL) || (text[0] == '\0')) return; // Security check
// We process the text lines one by one
if ((text != NULL) && (text[0] != '\0'))
{
// Get text lines ('\n' delimiter) to process lines individually
// NOTE: We can't use GuiTextSplit() because it can be already used before calling
// GuiDrawText() and static buffer would be overriden :(
// PROCEDURE:
// - Text is processed line per line
// - For every line, horizontal alignment is defined
// - For all text, vertical alignment is defined (multiline text only)
// - For every line, wordwrap mode is checked (useful for GuitextBox(), read-only)
// Get text lines (using '\n' as delimiter) to be processed individually
// WARNING: We can't use GuiTextSplit() function because it can be already used
// before the GuiDrawText() call and its buffer is static, it would be overriden :(
int lineCount = 0;
const char **lines = GetTextLines(text, &lineCount);
//Rectangle textBounds = GetTextBounds(LABEL, bounds);
// TextBox specific variables
int wrapMode = GuiGetStyle(TEXTBOX, TEXT_WRAP_MODE); // Wrap-mode only available in read-only mode, no for text editing
int alignmentVertical = GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT_VERTICAL);
// TODO: WARNING: This totalHeight is not valid for vertical alignment in case of word-wrap
float totalHeight = (float)(lineCount*GuiGetStyle(DEFAULT, TEXT_SIZE) + (lineCount - 1)*GuiGetStyle(DEFAULT, TEXT_SIZE)/2);
float posOffsetY = 0;
float posOffsetY = 0.0f;
for (int i = 0; i < lineCount; i++)
{
@ -4573,6 +4615,7 @@ static void GuiDrawText(const char *text, Rectangle bounds, int alignment, Color
switch (alignmentVertical)
{
// Only valid in case of wordWrap = 0;
case 0: boundsPos.y = bounds.y + posOffsetY + bounds.height/2 - totalHeight/2 + TEXT_VALIGN_PIXEL_OFFSET(bounds.height); break; // CENTERED
case 1: boundsPos.y = bounds.y + posOffsetY; break; // UP
case 2: boundsPos.y = bounds.y + posOffsetY + bounds.height - totalHeight + TEXT_VALIGN_PIXEL_OFFSET(bounds.height); break; // DOWN
@ -4598,133 +4641,9 @@ static void GuiDrawText(const char *text, Rectangle bounds, int alignment, Color
// Get size in bytes of text,
// considering end of line and line break
int lineSize = 0;
for (int c = 0; (lines[i][c] != '\0') && (lines[i][c] != '\n'); c++, lineSize++){ }
for (int c = 0; (lines[i][c] != '\0') && (lines[i][c] != '\n') && (lines[i][c] != '\r'); c++, lineSize++){ }
float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/guiFont.baseSize;
/*
// TODO: WARNING: For wordwrap, text must be measured from space to space before being drawn!
int wrapModeState = 0; // 0-TEXT_MEASURING, 1-TEXT_DRAWING
float textOffsetY = 0.0f; // Offset between wordwrap lines
float textOffsetX = 0.0f; // Offset X to next character to draw
int startLine = -1; // Index where to begin drawing (where a line begins)
int endLine = -1; // Index where to stop drawing (where a line ends)
int lastk = -1; // Holds last value of the character position
for (int i = 0, k = 0; i < lineSize; i++, k++)
{
// Get next codepoint from byte string and glyph index in font
int codepointByteCount = 0;
int codepoint = GetCodepoint(&text[i], &codepointByteCount);
int index = GetGlyphIndex(guiFont, codepoint);
// NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f)
// but we need to draw all of the bad bytes using the '?' symbol moving one byte
if (codepoint == 0x3f) codepointByteCount = 1;
i += (codepointByteCount - 1);
float glyphWidth = 0;
if (codepoint != '\n')
{
glyphWidth = (guiFont.glyphs[index].advanceX == 0)? guiFont.recs[index].width*scaleFactor : guiFont.glyphs[index].advanceX*scaleFactor;
if (i + 1 < lineSize) glyphWidth = glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING);
}
// NOTE: When wordWrap is ON we first measure how much of the text we can draw before going outside of the rec container
// We store this info in startLine and endLine, then we change states, draw the text between those two variables
// and change states again and again recursively until the end of the text (or until we get outside of the container).
// When wordWrap is OFF we don't need the measure state so we go to the drawing state immediately
// and begin drawing on the next line before we can get outside the container.
if (wrapModeState == 0) // TEXT_MEASURING
{
// TODO: There are multiple types of spaces in UNICODE, maybe it's a good idea to add support for more
// Ref: http://jkorpela.fi/chars/spaces.html
if ((codepoint == ' ') || (codepoint == '\t') || (codepoint == '\n')) endLine = i;
if ((textOffsetX + glyphWidth) > bounds.width)
{
endLine = (endLine < 1)? i : endLine;
if (i == endLine) endLine -= codepointByteCount;
if ((startLine + codepointByteCount) == endLine) endLine = (i - codepointByteCount);
wrapModeState = !wrapModeState;
}
else if ((i + 1) == lineSize)
{
endLine = i;
wrapModeState = !wrapModeState;
}
//else if (codepoint == '\n') state = !state;
if (wrapModeState == 1) // TEXT_DRAWING
{
textOffsetX = 0;
i = startLine;
glyphWidth = 0;
// Save character position when we switch states
int tmp = lastk;
lastk = k - 1;
k = tmp;
}
}
else
{
if (codepoint == '\n')
{
if (wrapMode != 2) // WORD_WRAP
{
textOffsetY += (guiFont.baseSize + guiFont.baseSize/2)*scaleFactor;
textOffsetX = 0;
}
}
else
{
if ((wrapMode == 1) && ((textOffsetX + glyphWidth) > bounds.width)) // CHAR_WRAP
{
textOffsetY += (guiFont.baseSize + guiFont.baseSize/2)*scaleFactor;
textOffsetX = 0;
}
// When text overflows rectangle height limit, just stop drawing
if ((textOffsetY + guiFont.baseSize*scaleFactor) > bounds.height) break;
// Draw text selected background
//bool isGlyphSelected = false;
//if ((selectStart >= 0) && (k >= selectStart) && (k < (selectStart + selectLength)))
//{
// DrawRectangleRec((Rectangle){ rec.x + textOffsetX - 1, rec.y + textOffsetY, glyphWidth, (float)font.baseSize*scaleFactor }, selectBackTint);
// isGlyphSelected = true;
//}
// Draw current character glyph
if ((codepoint != ' ') && (codepoint != '\t'))
{
DrawTextCodepoint(guiFont, codepoint, RAYGUI_CLITERAL(Vector2){ boundsPos.x + textOffsetX, boundsPos.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), tint); // isGlyphSelected? selectTint : tint);
}
}
if ((wrapMode == 2) && (i == endLine)) // WORD_WRAP
{
textOffsetY += (guiFont.baseSize + guiFont.baseSize/2)*scaleFactor;
textOffsetX = 0;
startLine = endLine;
endLine = -1;
glyphWidth = 0;
//selectStart += lastk - k;
k = lastk;
wrapModeState = !wrapModeState;
}
}
if ((textOffsetX != 0) || (codepoint != ' ')) textOffsetX += glyphWidth; // Avoid leading spaces
}
*/
int lastSpacePos = 0;
int textOffsetY = 0;
float textOffsetX = 0.0f;
for (int c = 0, codepointSize = 0; c < lineSize; c += codepointSize)
@ -4734,12 +4653,45 @@ static void GuiDrawText(const char *text, Rectangle bounds, int alignment, Color
// NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f)
// but we need to draw all of the bad bytes using the '?' symbol moving one byte
if (codepoint == 0x3f) codepointSize = 1;
if (codepoint == 0x3f) codepointSize = 1; // TODO: Review not recognized codepoints size
// Wrap mode text measuring to space to validate if it can be drawn or
// a new line is required
if (wrapMode == 1)
{
// Get glyph width to check if it goes out of bounds
float glyphWidth = 0;
if (guiFont.glyphs[index].advanceX == 0) glyphWidth = ((float)guiFont.recs[index].width*scaleFactor);
else glyphWidth = (float)guiFont.glyphs[index].advanceX*scaleFactor;
// Jump to next line if current character reach end of the box limits
if ((textOffsetX + glyphWidth) > bounds.width)
{
textOffsetX = 0.0f;
textOffsetY += GuiGetStyle(DEFAULT, TEXT_LINE_SPACING);
}
}
else if (wrapMode == 2) // 2-WORD_WRAP
{
// Get width to next space in line
int nextSpaceIndex = 0;
int nextSpaceWidth = GetNextSpaceWidth(lines[i] + c, &nextSpaceIndex);
if ((textOffsetX + nextSpaceWidth) > bounds.width)
{
textOffsetX = 0.0f;
textOffsetY += GuiGetStyle(DEFAULT, TEXT_LINE_SPACING);
}
// TODO: Consider case: (nextSpaceWidth >= bounds.width)
}
if (codepoint == '\n') break; // WARNING: Lines are already processed manually, no need to keep drawing after this codepoint
else
{
if ((codepoint != ' ') && (codepoint != '\t'))
// TODO: There are multiple types of spaces in Unicode,
// maybe it's a good idea to add support for more: http://jkorpela.fi/chars/spaces.html
if ((codepoint != ' ') && (codepoint != '\t')) // Do not draw codepoints with no glyph
{
if (wrapMode == 0) // 0-NO_WRAP
{
@ -4749,40 +4701,28 @@ static void GuiDrawText(const char *text, Rectangle bounds, int alignment, Color
DrawTextCodepoint(guiFont, codepoint, RAYGUI_CLITERAL(Vector2){ boundsPos.x + textOffsetX, boundsPos.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha));
}
}
else if (wrapMode == 1) // 1-CHAR_WRAP
else if (wrapMode >= 1) // 2-WORD_WRAP
{
// Get glyph width to check if it goes out of bounds
float glyphWidth = 0;
if (guiFont.glyphs[index].advanceX == 0) glyphWidth = ((float)guiFont.recs[index].width*scaleFactor);
else glyphWidth = (float)guiFont.glyphs[index].advanceX*scaleFactor;
// Jump to next line if current character reach end of the box limits
if ((boundsPos.x + textOffsetX + glyphWidth) > (bounds.x + bounds.width))
// Draw only glyphs inside the bounds
if ((boundsPos.y + textOffsetY) <= (bounds.y + bounds.height - GuiGetStyle(DEFAULT, TEXT_SIZE)))
{
textOffsetX = 0.0f;
textOffsetY += GuiGetStyle(DEFAULT, TEXT_LINE_SPACING);
DrawTextCodepoint(guiFont, codepoint, RAYGUI_CLITERAL(Vector2){ boundsPos.x + textOffsetX, boundsPos.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha));
}
}
else if (wrapMode == 2) // 2-WORD_WRAP
{
// TODO: Word wrap mode requires previously measured text to last space!
}
}
else lastSpacePos = c;
if (guiFont.glyphs[index].advanceX == 0) textOffsetX += ((float)guiFont.recs[index].width*scaleFactor + (float)GuiGetStyle(DEFAULT, TEXT_SPACING));
else textOffsetX += ((float)guiFont.glyphs[index].advanceX*scaleFactor + (float)GuiGetStyle(DEFAULT, TEXT_SPACING));
}
}
posOffsetY += (float)GuiGetStyle(DEFAULT, TEXT_LINE_SPACING);
if (wrapMode == 0) posOffsetY += (float)GuiGetStyle(DEFAULT, TEXT_LINE_SPACING);
else if (wrapMode >= 1) posOffsetY += (textOffsetY + (float)GuiGetStyle(DEFAULT, TEXT_LINE_SPACING));
//---------------------------------------------------------------------------------
}
}
#if defined(RAYGUI_DEBUG_TEXT_BOUNDS)
GuiDrawRectangle(bounds, 0, WHITE, Fade(RED, 0.4f));
GuiDrawRectangle(bounds, 0, WHITE, Fade(BLUE, 0.4f));
#endif
}
@ -4803,6 +4743,10 @@ static void GuiDrawRectangle(Rectangle rec, int borderWidth, Color borderColor,
DrawRectangle((int)rec.x + (int)rec.width - borderWidth, (int)rec.y + borderWidth, borderWidth, (int)rec.height - 2*borderWidth, GuiFade(borderColor, guiAlpha));
DrawRectangle((int)rec.x, (int)rec.y + (int)rec.height - borderWidth, (int)rec.width, borderWidth, GuiFade(borderColor, guiAlpha));
}
#if defined(RAYGUI_DEBUG_RECS_BOUNDS)
DrawRectangle((int)rec.x, (int)rec.y, (int)rec.width, (int)rec.height, Fade(RED, 0.4f));
#endif
}
// Draw tooltip using control bounds