From b1f8cde32992db160799ed1424951618c6f3ea47 Mon Sep 17 00:00:00 2001 From: David Buzatto Date: Wed, 3 Dec 2025 05:44:18 -0300 Subject: [PATCH] [examples] Added: `text_strings_management` (#5379) * new shapes example - penrose tile * stack cleanup * proper use of strnlen, strncat and strncpy * typo correction * update screenshot of shapes_penrose_tile example * new example for strings management * Improved structure for text_strings_management --- examples/text/text_strings_management.c | 400 ++++++++++++++++++++++ examples/text/text_strings_management.png | Bin 0 -> 18431 bytes 2 files changed, 400 insertions(+) create mode 100644 examples/text/text_strings_management.c create mode 100644 examples/text/text_strings_management.png diff --git a/examples/text/text_strings_management.c b/examples/text/text_strings_management.c new file mode 100644 index 000000000..d6b4aeb57 --- /dev/null +++ b/examples/text/text_strings_management.c @@ -0,0 +1,400 @@ +/******************************************************************************************* +* +* raylib [text] example - strings management +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.6-dev, last time updated with raylib 5.6-dev +* +* Example contributed by David Buzatto (@davidbuzatto) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 David Buzatto (@davidbuzatto) +* +********************************************************************************************/ + +#include "raylib.h" + +#include + +#define MAX_TEXT_LENGTH 100 +#define MAX_TEXT_PARTICLES 100 +#define FONT_SIZE 30 + +//---------------------------------------------------------------------------------- +// Types and Structures Definition +//---------------------------------------------------------------------------------- +typedef struct TextParticle { + char text[MAX_TEXT_LENGTH]; + Rectangle rect; // Boundary + Vector2 vel; // Velocity + Vector2 ppos; // Previous position + float padding; + float borderWidth; + float friction; + float elasticity; + Color color; + bool grabbed; +} TextParticle; + +//---------------------------------------------------------------------------------- +// Module Functions Declaration +//---------------------------------------------------------------------------------- +void PrepareFirstTextParticle(const char* text, TextParticle *tps, int *particleCount); +TextParticle CreateTextParticle(const char *text, float x, float y, Color color); +void SliceTextParticle(TextParticle *tp, int particlePos, int sliceLength, TextParticle *tps, int *particleCount); +void SliceTextParticleByChar(TextParticle *tp, char charToSlice, TextParticle *tps, int *particleCount); +void ShatterTextParticle(TextParticle *tp, int particlePos, TextParticle *tps, int *particleCount); +void GlueTextParticles(TextParticle *grabbed, TextParticle *target, TextParticle *tps, int *particleCount); +void RealocateTextParticles(TextParticle *tps, int particlePos, int *particleCount); + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - strings management"); + + TextParticle textParticles[MAX_TEXT_PARTICLES] = { 0 }; + int particleCount = 0; + TextParticle *grabbedTextParticle = NULL; + Vector2 pressOffset = {0}; + + PrepareFirstTextParticle("raylib => fun videogames programming!", textParticles, &particleCount); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //--------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + float delta = GetFrameTime(); + Vector2 mousePos = GetMousePosition(); + + // Checks if a text particle was grabbed + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) + { + for (int i = particleCount - 1; i >= 0; i--) + { + TextParticle *tp = &textParticles[i]; + pressOffset.x = mousePos.x - tp->rect.x; + pressOffset.y = mousePos.y - tp->rect.y; + if (CheckCollisionPointRec(mousePos, tp->rect)) + { + tp->grabbed = true; + grabbedTextParticle = tp; + break; + } + } + } + + // Releases any text particle the was grabbed + if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) + { + if (grabbedTextParticle != NULL) + { + grabbedTextParticle->grabbed = false; + grabbedTextParticle = NULL; + } + } + + // Slice os shatter a text particle + if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) + { + for (int i = particleCount - 1; i >= 0; i--) + { + TextParticle *tp = &textParticles[i]; + if (CheckCollisionPointRec(mousePos, tp->rect)) + { + if (IsKeyDown(KEY_LEFT_SHIFT)) + { + ShatterTextParticle(tp, i, textParticles, &particleCount); + } + else + { + SliceTextParticle(tp, i, TextLength(tp->text)/2, textParticles, &particleCount); + } + break; + } + } + } + + // Shake text particles + if (IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE)) + { + for (int i = 0; i < particleCount; i++) + { + if (!textParticles[i].grabbed) textParticles[i].vel = (Vector2){ GetRandomValue(-2000, 2000), GetRandomValue(-2000, 2000) }; + } + } + + // Reset using TextTo* functions + if (IsKeyPressed(KEY_ONE)) PrepareFirstTextParticle("raylib => fun videogames programming!", textParticles, &particleCount); + if (IsKeyPressed(KEY_TWO)) PrepareFirstTextParticle(TextToUpper("raylib => fun videogames programming!"), textParticles, &particleCount); + if (IsKeyPressed(KEY_THREE)) PrepareFirstTextParticle(TextToLower("raylib => fun videogames programming!"), textParticles, &particleCount); + if (IsKeyPressed(KEY_FOUR)) PrepareFirstTextParticle(TextToPascal("raylib_fun_videogames_programming"), textParticles, &particleCount); + if (IsKeyPressed(KEY_FIVE)) PrepareFirstTextParticle(TextToSnake("RaylibFunVideogamesProgramming"), textParticles, &particleCount); + if (IsKeyPressed(KEY_SIX)) PrepareFirstTextParticle(TextToCamel("raylib_fun_videogames_programming"), textParticles, &particleCount); + + // Slice by char pressed only when we have one text particle + char charPressed = GetCharPressed(); + if ((charPressed >= 'A') && (charPressed <= 'z') && (particleCount == 1)) + { + SliceTextParticleByChar(&textParticles[0], charPressed, textParticles, &particleCount); + } + + // Updates each text particle state + for (int i = 0; i < particleCount; i++) + { + TextParticle *tp = &textParticles[i]; + + // The text particle is not grabbed + if (!tp->grabbed) + { + // text particle repositioning using the velocity + tp->rect.x += tp->vel.x * delta; + tp->rect.y += tp->vel.y * delta; + + // Does the text particle hit the screen right boundary? + if ((tp->rect.x + tp->rect.width) >= screenWidth) + { + tp->rect.x = screenWidth - tp->rect.width; // Text particle repositioning + tp->vel.x = -tp->vel.x*tp->elasticity; // Elasticity makes the text particle lose 10% of its velocity on hit + } + // Does the text particle hit the screen left boundary? + else if (tp->rect.x <= 0) + { + tp->rect.x = 0.0f; + tp->vel.x = -tp->vel.x*tp->elasticity; + } + + // The same for y axis + if ((tp->rect.y + tp->rect.height) >= screenHeight) + { + tp->rect.y = screenHeight - tp->rect.height; + tp->vel.y = -tp->vel.y*tp->elasticity; + } + else if (tp->rect.y <= 0) + { + tp->rect.y = 0.0f; + tp->vel.y = -tp->vel.y*tp->elasticity; + } + + // Friction makes the text particle lose 1% of its velocity each frame + tp->vel.x = tp->vel.x*tp->friction; + tp->vel.y = tp->vel.y*tp->friction; + } + else + { + // Text particle repositioning using the mouse position + tp->rect.x = mousePos.x - pressOffset.x; + tp->rect.y = mousePos.y - pressOffset.y; + + // While the text particle is grabbed, recalculates its velocity + tp->vel.x = (tp->rect.x - tp->ppos.x)/delta; + tp->vel.y = (tp->rect.y - tp->ppos.y)/delta; + tp->ppos.x = tp->rect.x; + tp->ppos.y = tp->rect.y; + + // Glue text particles when dragging and pressing left ctrl + if (IsKeyDown(KEY_LEFT_CONTROL)) + { + for (int i = 0; i < particleCount; i++) + { + if (&textParticles[i] != grabbedTextParticle && grabbedTextParticle->grabbed) + { + if (CheckCollisionRecs(grabbedTextParticle->rect, textParticles[i].rect)) + { + GlueTextParticles(grabbedTextParticle, &textParticles[i], textParticles, &particleCount); + grabbedTextParticle = &textParticles[particleCount-1]; + } + } + } + } + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + for (int i = 0; i < particleCount; i++) + { + TextParticle *tp = &textParticles[i]; + DrawRectangle(tp->rect.x-tp->borderWidth, tp->rect.y-tp->borderWidth, tp->rect.width+tp->borderWidth*2, tp->rect.height+tp->borderWidth*2, BLACK); + DrawRectangleRec(tp->rect, tp->color); + DrawText(tp->text, tp->rect.x+tp->padding, tp->rect.y+tp->padding, FONT_SIZE, BLACK); + } + + DrawText("grab a text particle by pressing with the mouse and throw it by releasing", 10, 10, 10, DARKGRAY); + DrawText("slice a text particle by pressing it with the mouse right button", 10, 30, 10, DARKGRAY); + DrawText("shatter a text particle keeping left shift pressed and pressing it with the mouse right button", 10, 50, 10, DARKGRAY); + DrawText("glue text particles by grabbing than and keeping left control pressed", 10, 70, 10, DARKGRAY); + DrawText("1 to 6 to reset", 10, 90, 10, DARKGRAY); + DrawText("when you have only one text particle, you can slice it by pressing a char", 10, 110, 10, DARKGRAY); + DrawText(TextFormat("TEXT PARTICLE COUNT: %d", particleCount), 10, GetScreenHeight() - 30, 20, BLACK); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} + +//---------------------------------------------------------------------------------- +// Module Functions Definition +//---------------------------------------------------------------------------------- +void PrepareFirstTextParticle(const char* text, TextParticle *tps, int *particleCount) +{ + tps[0] = CreateTextParticle( + text, + GetScreenWidth()/2, + GetScreenHeight()/2, + RAYWHITE + ); + *particleCount = 1; +} + +TextParticle CreateTextParticle(const char *text, float x, float y, Color color) +{ + TextParticle tp = { + .text = "", + .rect = { x, y, 30, 30 }, + .vel = { GetRandomValue(-200, 200), GetRandomValue(-200, 200) }, + .ppos = { 0 }, + .padding = 5.0f, + .borderWidth = 5.0f, + .friction = 0.99, + .elasticity = 0.9, + .color = color, + .grabbed = false + }; + + TextCopy(tp.text, text); + tp.rect.width = MeasureText(tp.text, FONT_SIZE)+tp.padding*2; + tp.rect.height = FONT_SIZE+tp.padding*2; + return tp; +} + +void SliceTextParticle(TextParticle *tp, int particlePos, int sliceLength, TextParticle *tps, int *particleCount) +{ + int length = TextLength(tp->text); + + if((length > 1) && ((*particleCount+length) < MAX_TEXT_PARTICLES)) + { + for (int i = 0; i < length; i += sliceLength) + { + const char *text = sliceLength == 1 ? TextFormat("%c", tp->text[i]) : TextSubtext(tp->text, i, sliceLength); + tps[(*particleCount)++] = CreateTextParticle( + text, + tp->rect.x + i * tp->rect.width/length, + tp->rect.y, + (Color) { GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255 } + ); + } + RealocateTextParticles(tps, particlePos, particleCount); + } +} + +void SliceTextParticleByChar(TextParticle *tp, char charToSlice, TextParticle *tps, int *particleCount) +{ + int tokenCount = 0; + const char **tokens = TextSplit(tp->text, charToSlice, &tokenCount); + + if (tokenCount > 1) + { + int textLength = TextLength(tp->text); + for (int i = 0; i < textLength; i++) + { + if (tp->text[i] == charToSlice) + { + tps[(*particleCount)++] = CreateTextParticle( + TextFormat("%c", charToSlice), + tp->rect.x, + tp->rect.y, + (Color) { GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255 } + ); + } + } + for (int i = 0; i < tokenCount; i++) + { + int tokenLength = TextLength(tokens[i]); + tps[(*particleCount)++] = CreateTextParticle( + TextFormat("%s", tokens[i]), + tp->rect.x + i * tp->rect.width/tokenLength, + tp->rect.y, + (Color) { GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255 } + ); + } + if (tokenCount) + { + RealocateTextParticles(tps, 0, particleCount); + } + } +} + +void ShatterTextParticle(TextParticle *tp, int particlePos, TextParticle *tps, int *particleCount) +{ + SliceTextParticle(tp, particlePos, 1, tps, particleCount); +} + +void GlueTextParticles(TextParticle *grabbed, TextParticle *target, TextParticle *tps, int *particleCount) +{ + int p1 = -1; + int p2 = -1; + + for (int i = 0; i < *particleCount; i++) + { + if (&tps[i] == grabbed) p1 = i; + if (&tps[i] == target) p2 = i; + } + + if ((p1 != -1) && (p2 != -1)) + { + TextParticle tp = CreateTextParticle( + TextFormat( "%s%s", grabbed->text, target->text), + grabbed->rect.x, + grabbed->rect.y, + RAYWHITE + ); + tp.grabbed = true; + tps[(*particleCount)++] = tp; + grabbed->grabbed = false; + if (p1 < p2) + { + RealocateTextParticles(tps, p2, particleCount); + RealocateTextParticles(tps, p1, particleCount); + } + else + { + RealocateTextParticles(tps, p1, particleCount); + RealocateTextParticles(tps, p2, particleCount); + } + } +} + +void RealocateTextParticles(TextParticle *tps, int particlePos, int *particleCount) +{ + for (int i = particlePos+1; i < *particleCount; i++) + { + tps[i-1] = tps[i]; + } + (*particleCount)--; +} \ No newline at end of file diff --git a/examples/text/text_strings_management.png b/examples/text/text_strings_management.png new file mode 100644 index 0000000000000000000000000000000000000000..d9b6cc4ed83dfd2eadef9b2b06725a4ccae864cc GIT binary patch literal 18431 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