SDL: checkkeys will now render text that is input

From e9d5060c4c2df66e8d4ef4f748f80e8ef32fc233 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 1 Jul 2022 12:56:47 -0700
Subject: [PATCH] checkkeys will now render text that is input

Also added test functions for multi-line debug text display

Currently this only supports ASCII, as the font doesn't have the correct Latin-1 characters
---
 VisualC/tests/checkkeys/checkkeys.vcxproj |   8 +-
 include/SDL_test_font.h                   |  93 ++++++++-
 src/test/SDL_test_font.c                  | 223 +++++++++++++++++++++-
 test/checkkeys.c                          |  40 +++-
 4 files changed, 346 insertions(+), 18 deletions(-)

diff --git a/VisualC/tests/checkkeys/checkkeys.vcxproj b/VisualC/tests/checkkeys/checkkeys.vcxproj
index b9d4f9b21a8..3f068377b97 100644
--- a/VisualC/tests/checkkeys/checkkeys.vcxproj
+++ b/VisualC/tests/checkkeys/checkkeys.vcxproj
@@ -200,6 +200,12 @@
       <CopyLocalSatelliteAssemblies>false</CopyLocalSatelliteAssemblies>
       <ReferenceOutputAssembly>true</ReferenceOutputAssembly>
     </ProjectReference>
+    <ProjectReference Include="..\..\SDLtest\SDLtest.vcxproj">
+      <Project>{da956fd3-e143-46f2-9fe5-c77bebc56b1a}</Project>
+      <Private>false</Private>
+      <CopyLocalSatelliteAssemblies>false</CopyLocalSatelliteAssemblies>
+      <ReferenceOutputAssembly>true</ReferenceOutputAssembly>
+    </ProjectReference>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="..\..\..\test\checkkeys.c">
@@ -216,4 +222,4 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/include/SDL_test_font.h b/include/SDL_test_font.h
index c5cbbbbd341..ea50e88ac5c 100644
--- a/include/SDL_test_font.h
+++ b/include/SDL_test_font.h
@@ -38,7 +38,8 @@ extern "C" {
 
 /* Function prototypes */
 
-#define FONT_CHARACTER_SIZE  8
+#define FONT_CHARACTER_SIZE 8
+#define FONT_LINE_HEIGHT    (FONT_CHARACTER_SIZE + 2)
 
 /**
  *  \brief Draw a string in the currently set font.
@@ -50,10 +51,12 @@ extern "C" {
  *
  *  \returns 0 on success, -1 on failure.
  */
-int SDLTest_DrawCharacter(SDL_Renderer *renderer, int x, int y, char c);
+int SDLTest_DrawCharacter(SDL_Renderer *renderer, int x, int y, Uint32 c);
 
 /**
- *  \brief Draw a string in the currently set font.
+ *  \brief Draw a UTF-8 string in the currently set font.
+ *
+ *  The font currently only supports characters in the Basic Latin and Latin-1 Supplement sets.
  *
  *  \param renderer The renderer to draw on.
  *  \param x The X coordinate of the upper left corner of the string.
@@ -64,6 +67,90 @@ int SDLTest_DrawCharacter(SDL_Renderer *renderer, int x, int y, char c);
  */
 int SDLTest_DrawString(SDL_Renderer *renderer, int x, int y, const char *s);
 
+/**
+ *  \brief Data used for multi-line text output
+ */
+typedef struct SDLTest_TextWindow
+{
+    SDL_Rect rect;
+    int current;
+    int numlines;
+    char **lines;
+} SDLTest_TextWindow;
+
+/**
+ *  \brief Create a multi-line text output window
+ *
+ *  \param x The X coordinate of the upper left corner of the window.
+ *  \param y The Y coordinate of the upper left corner of the window.
+ *  \param w The width of the window (currently ignored)
+ *  \param h The height of the window (currently ignored)
+ *
+ *  \returns the new window, or NULL on failure.
+ *
+ *  \since This function is available since SDL 2.24.0
+ */
+SDLTest_TextWindow *SDLTest_TextWindowCreate(int x, int y, int w, int h);
+
+/**
+ *  \brief Display a multi-line text output window
+ *
+ *  This function should be called every frame to display the text
+ *
+ *  \param textwin The text output window
+ *  \param renderer The renderer to use for display
+ *
+ *  \since This function is available since SDL 2.24.0
+ */
+void SDLTest_TextWindowDisplay(SDLTest_TextWindow *textwin, SDL_Renderer *renderer);
+
+/**
+ *  \brief Add text to a multi-line text output window
+ *
+ *  Adds UTF-8 text to the end of the current text. The '\n' newline character starts a
+ *  new line of text. The '\b' backspace character deletes the last character or, if the
+ *  line is empty, deletes the line and goes to the end of the previous line.
+ *
+ *  \param textwin The text output window
+ *  \param fmt A printf() style format string
+ *  \param ...  additional parameters matching % tokens in the `fmt` string, if any
+ *
+ *  \since This function is available since SDL 2.24.0
+ */
+void SDLTest_TextWindowAddText(SDLTest_TextWindow *textwin, SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(2);
+
+/**
+ *  \brief Add text to a multi-line text output window
+ *
+ *  Adds UTF-8 text to the end of the current text. The '\n' newline character starts a
+ *  new line of text. The '\b' backspace character deletes the last character or, if the
+ *  line is empty, deletes the line and goes to the end of the previous line.
+ *
+ *  \param textwin The text output window
+ *  \param text The text to add to the window
+ *  \param len The length, in bytes, of the text to add to the window
+ *
+ *  \since This function is available since SDL 2.24.0
+ */
+void SDLTest_TextWindowAddTextWithLength(SDLTest_TextWindow *textwin, const char *text, size_t len);
+
+/**
+ *  \brief Clear the text in a multi-line text output window
+ *
+ *  \param textwin The text output window
+ *
+ *  \since This function is available since SDL 2.24.0
+ */
+void SDLTest_TextWindowClear(SDLTest_TextWindow *textwin);
+
+/**
+ *  \brief Free the storage associated with a multi-line text output window
+ *
+ *  \param textwin The text output window
+ *
+ *  \since This function is available since SDL 2.24.0
+ */
+void SDLTest_TextWindowDestroy(SDLTest_TextWindow *textwin);
 
 /**
  *  \brief Cleanup textures used by font drawing functions.
diff --git a/src/test/SDL_test_font.c b/src/test/SDL_test_font.c
index 4129f3da2bd..d7ed98deabc 100644
--- a/src/test/SDL_test_font.c
+++ b/src/test/SDL_test_font.c
@@ -3120,7 +3120,7 @@ struct SDLTest_CharTextureCache {
 */
 static struct SDLTest_CharTextureCache *SDLTest_CharTextureCacheList;
 
-int SDLTest_DrawCharacter(SDL_Renderer *renderer, int x, int y, char c)
+int SDLTest_DrawCharacter(SDL_Renderer *renderer, int x, int y, Uint32 c)
 {
     const Uint32 charWidth = FONT_CHARACTER_SIZE;
     const Uint32 charHeight = FONT_CHARACTER_SIZE;
@@ -3156,7 +3156,7 @@ int SDLTest_DrawCharacter(SDL_Renderer *renderer, int x, int y, char c)
     drect.h = charHeight;
 
     /* Character index in cache */
-    ci = (unsigned char)c;
+    ci = c;
 
     /* Search for this renderer's cache */
     for (cache = SDLTest_CharTextureCacheList; cache != NULL; cache = cache->next) {
@@ -3242,23 +3242,234 @@ int SDLTest_DrawCharacter(SDL_Renderer *renderer, int x, int y, char c)
     return (result);
 }
 
+/* Gets a unicode value from a UTF-8 encoded string
+ * Outputs increment to advance the string */
+#define UNKNOWN_UNICODE 0xFFFD
+static Uint32 UTF8_getch(const char *src, size_t srclen, int *inc)
+{
+    const Uint8 *p = (const Uint8 *)src;
+    size_t left = 0;
+    size_t save_srclen = srclen;
+    SDL_bool overlong = SDL_FALSE;
+    SDL_bool underflow = SDL_FALSE;
+    Uint32 ch = UNKNOWN_UNICODE;
+
+    if (srclen == 0) {
+        return UNKNOWN_UNICODE;
+    }
+    if (p[0] >= 0xFC) {
+        if ((p[0] & 0xFE) == 0xFC) {
+            if (p[0] == 0xFC && (p[1] & 0xFC) == 0x80) {
+                overlong = SDL_TRUE;
+            }
+            ch = (Uint32) (p[0] & 0x01);
+            left = 5;
+        }
+    } else if (p[0] >= 0xF8) {
+        if ((p[0] & 0xFC) == 0xF8) {
+            if (p[0] == 0xF8 && (p[1] & 0xF8) == 0x80) {
+                overlong = SDL_TRUE;
+            }
+            ch = (Uint32) (p[0] & 0x03);
+            left = 4;
+        }
+    } else if (p[0] >= 0xF0) {
+        if ((p[0] & 0xF8) == 0xF0) {
+            if (p[0] == 0xF0 && (p[1] & 0xF0) == 0x80) {
+                overlong = SDL_TRUE;
+            }
+            ch = (Uint32) (p[0] & 0x07);
+            left = 3;
+        }
+    } else if (p[0] >= 0xE0) {
+        if ((p[0] & 0xF0) == 0xE0) {
+            if (p[0] == 0xE0 && (p[1] & 0xE0) == 0x80) {
+                overlong = SDL_TRUE;
+            }
+            ch = (Uint32) (p[0] & 0x0F);
+            left = 2;
+        }
+    } else if (p[0] >= 0xC0) {
+        if ((p[0] & 0xE0) == 0xC0) {
+            if ((p[0] & 0xDE) == 0xC0) {
+                overlong = SDL_TRUE;
+            }
+            ch = (Uint32) (p[0] & 0x1F);
+            left = 1;
+        }
+    } else {
+        if ((p[0] & 0x80) == 0x00) {
+            ch = (Uint32) p[0];
+        }
+    }
+    --srclen;
+    while (left > 0 && srclen > 0) {
+        ++p;
+        if ((p[0] & 0xC0) != 0x80) {
+            ch = UNKNOWN_UNICODE;
+            break;
+        }
+        ch <<= 6;
+        ch |= (p[0] & 0x3F);
+        --srclen;
+        --left;
+    }
+    if (left > 0) {
+        underflow = SDL_TRUE;
+    }
+
+    if (overlong || underflow ||
+        (ch >= 0xD800 && ch <= 0xDFFF) ||
+        (ch == 0xFFFE || ch == 0xFFFF) || ch > 0x10FFFF) {
+        ch = UNKNOWN_UNICODE;
+    }
+
+    *inc = (int)(save_srclen - srclen);
+
+    return ch;
+}
+
+#define UTF8_IsTrailingByte(c) ((c) >= 0x80 && (c) <= 0xBF)
+
 int SDLTest_DrawString(SDL_Renderer * renderer, int x, int y, const char *s)
 {
     const Uint32 charWidth = FONT_CHARACTER_SIZE;
     int result = 0;
     int curx = x;
     int cury = y;
-    const char *curchar = s;
+    size_t len = SDL_strlen(s);
 
-    while (*curchar && !result) {
-        result |= SDLTest_DrawCharacter(renderer, curx, cury, *curchar);
+    while (len > 0 && !result) {
+        int advance = 0;
+        Uint32 ch = UTF8_getch(s, len, &advance);
+        if (ch < 256) {
+            result |= SDLTest_DrawCharacter(renderer, curx, cury, ch);
+        }
         curx += charWidth;
-        curchar++;
+        s += advance;
+        len -= advance;
     }
 
     return (result);
 }
 
+SDLTest_TextWindow *SDLTest_TextWindowCreate(int x, int y, int w, int h)
+{
+    SDLTest_TextWindow *textwin = (SDLTest_TextWindow *)SDL_malloc(sizeof(*textwin));
+
+    if ( !textwin ) {
+        return NULL;
+    }
+
+    textwin->rect.x = x;
+    textwin->rect.y = y;
+    textwin->rect.w = w;
+    textwin->rect.h = h;
+    textwin->current = 0;
+    textwin->numlines = (h / FONT_LINE_HEIGHT);
+    textwin->lines = (char **)SDL_calloc(textwin->numlines, sizeof(*textwin->lines));
+    if ( !textwin->lines ) {
+        SDL_free(textwin);
+        return NULL;
+    }
+    return textwin;
+}
+
+void SDLTest_TextWindowDisplay(SDLTest_TextWindow *textwin, SDL_Renderer *renderer)
+{
+    int i, y;
+
+    for ( y = textwin->rect.y, i = 0; i < textwin->numlines; ++i, y += FONT_LINE_HEIGHT ) {
+        if ( textwin->lines[i] ) {
+            SDLTest_DrawString(renderer, textwin->rect.x, y, textwin->lines[i]);
+        }
+    }
+}
+
+void SDLTest_TextWindowAddText(SDLTest_TextWindow *textwin, const char *fmt, ...)
+{
+	char text[1024];
+	va_list ap;
+
+	va_start(ap, fmt);
+	SDL_vsnprintf(text, sizeof(text), fmt, ap);
+	va_end(ap);
+
+    SDLTest_TextWindowAddTextWithLength(textwin, text, SDL_strlen(text));
+}
+
+void SDLTest_TextWindowAddTextWithLength(SDLTest_TextWindow *textwin, const char *text, size_t len)
+{
+    size_t existing;
+    SDL_bool newline = SDL_FALSE;
+    char *line;
+
+    if ( len > 0 && text[len - 1] == '\n' ) {
+        --len;
+        newline = SDL_TRUE;
+    }
+
+    if ( textwin->lines[textwin->current] ) {
+        existing = SDL_strlen(textwin->lines[textwin->current]);
+    } else {
+        existing = 0;
+    }
+
+    if ( *text == '\b' ) {
+        if ( existing ) {
+            while (existing > 1 && UTF8_IsTrailingByte((Uint8)textwin->lines[textwin->current][existing - 1])) {
+                --existing;
+            }
+            --existing;
+            textwin->lines[textwin->current][existing] = '\0';
+        } else if (textwin->current > 0) {
+            SDL_free(textwin->lines[textwin->current]);
+            textwin->lines[textwin->current] = NULL;
+            --textwin->current;
+        }
+        return;
+    }
+
+    line = (char *)SDL_realloc(textwin->lines[textwin->current], existing + len + 1);
+    if ( line ) {
+        SDL_memcpy(&line[existing], text, len);
+        line[existing + len] = '\0';
+        textwin->lines[textwin->current] = line;
+        if ( newline ) {
+            if (textwin->current == textwin->numlines - 1) {
+                SDL_free(textwin->lines[0]);
+                SDL_memcpy(&textwin->lines[0], &textwin->lines[1], (textwin->numlines-1) * sizeof(textwin->lines[1]));
+                textwin->lines[textwin->current] = NULL;
+            } else {
+                ++textwin->current;
+            }
+        }
+    }
+}
+
+void SDLTest_TextWindowClear(SDLTest_TextWindow *textwin)
+{
+    int i;
+
+    for ( i = 0; i < textwin->numlines; ++i )
+    {
+        if ( textwin->lines[i] ) {
+            SDL_free(textwin->lines[i]);
+            textwin->lines[i] = NULL;
+        }
+    }
+    textwin->current = 0;
+}
+
+void SDLTest_TextWindowDestroy(SDLTest_TextWindow *textwin)
+{
+    if ( textwin ) {
+        SDLTest_TextWindowClear(textwin);
+        SDL_free(textwin->lines);
+        SDL_free(textwin);
+    }
+}
+
 void SDLTest_CleanupTextDrawing(void)
 {
     unsigned int i;
diff --git a/test/checkkeys.c b/test/checkkeys.c
index 010360278f2..385b80629d0 100644
--- a/test/checkkeys.c
+++ b/test/checkkeys.c
@@ -24,8 +24,12 @@
 #endif
 
 #include "SDL.h"
+#include "SDL_test_font.h"
 
-int done;
+static SDL_Window *window;
+static SDL_Renderer *renderer;
+static SDLTest_TextWindow *textwin;
+static int done;
 
 /* Call this instead of exit(), so we can clean up SDL: atexit() is evil. */
 static void
@@ -163,6 +167,18 @@ loop()
         case SDL_KEYDOWN:
         case SDL_KEYUP:
             PrintKey(&event.key.keysym, (event.key.state == SDL_PRESSED) ? SDL_TRUE : SDL_FALSE, (event.key.repeat) ? SDL_TRUE : SDL_FALSE);
+            if (event.type == SDL_KEYDOWN) {
+                switch (event.key.keysym.sym) {
+                case SDLK_BACKSPACE:
+                    SDLTest_TextWindowAddText(textwin, "\b");
+                    break;
+                case SDLK_RETURN:
+                    SDLTest_TextWindowAddText(textwin, "\n");
+                    break;
+                default:
+                    break;
+                }
+            }
             break;
         case SDL_TEXTEDITING:
             PrintText("EDIT", event.edit.text);
@@ -173,6 +189,7 @@ loop()
             break;
         case SDL_TEXTINPUT:
             PrintText("INPUT", event.text.text);
+            SDLTest_TextWindowAddText(textwin, "%s", event.text.text);
             break;
         case SDL_FINGERDOWN:
             if (SDL_IsTextInputActive()) {
@@ -204,6 +221,13 @@ loop()
             break;
         }
     }
+
+    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+    SDL_RenderClear(renderer);
+    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+    SDLTest_TextWindowDisplay(textwin, renderer);
+    SDL_RenderPresent(renderer);
+
 #ifdef __EMSCRIPTEN__
     if (done) {
         emscripten_cancel_main_loop();
@@ -214,9 +238,6 @@ loop()
 int
 main(int argc, char *argv[])
 {
-    SDL_Window *window;
-    SDL_Renderer *renderer;
-
     /* Enable standard application logging */
     SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
 
@@ -242,11 +263,14 @@ main(int argc, char *argv[])
         quit(2);
     }
 
-    /* On wayland, no window will actually show until something has
-       actually been displayed.
-    */
     renderer = SDL_CreateRenderer(window, -1, 0);
-    SDL_RenderPresent(renderer);
+    if (!renderer) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create renderer: %s\n",
+                SDL_GetError());
+        quit(2);
+    }
+
+    textwin = SDLTest_TextWindowCreate(0, 0, 640, 480);
 
 #if __IPHONEOS__
     /* Creating the context creates the view, which we need to show keyboard */