#include "shell/Shell.h" #include "shell/ShellCallbacks.h" #include "shell/ShellKeyCodes.h" #include "3dmodelio/TextureAtlasData.h" #include "gamemath/Matrix4x4f.h" #include "gamemath/MouseCoordinateTransforms.h" #include "font/BitmapFont.h" #include "font/TextFlow.h" #include "jsonserialization/JSONDeserializationContext.h" #include "renderer/Drawing.h" #include "renderer/Renderable.h" #include "renderer/Renderer.h" #include "shadercollection/ShaderCollection.h" #include "shadercollection/ShaderConfiguration2DTexture.h" #include "imageio/ImageIO.h" #include "utilities/AutoFreePool.h" #include "utilities/IOUtilities.h" #if defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos) #include "eaglshell/EAGLShell.h" #include "eaglshell/EAGLTarget.h" #elif defined(STEM_PLATFORM_macosx) #include "nsopenglshell/NSOpenGLShell.h" #include "nsopenglshell/NSOpenGLTarget.h" #elif defined(STEM_PLATFORM_windows) #include "wglshell/WGLShell.h" #include "wglshell/WGLTarget.h" #elif defined(STEM_PLATFORM_linux) #include "glxshell/GLXShell.h" #include "glxshell/GLXTarget.h" #elif defined(STEM_PLATFORM_android) #include "eglshell/EGLShell.h" #include "eglshell/EGLTarget.h" #else #error Unsupported platform #endif #include #include #include #include #define FREEFORM_LENGTH_MAX 100 static char freeformText[FREEFORM_LENGTH_MAX + 1]; static unsigned int viewportWidth = 800, viewportHeight = 600; static float viewportRatio = (800.0f / 600.0f); static BitmapFont * font; static TextFlow * textFlow; static const char * jsonPath = NULL; static size_t lastIndexAtPositionX = 0; static bool lastLeadingEdge = false; static Vector2f lastPositionAtIndex = {0.0f, -1.0f}; static float scale = 1.0f; static bool draggingTextFlow, draggingClipBounds1, draggingClipBounds2; static float lastDragX, lastDragY; static float textFlowWidth; static Rect4f clipBounds; static bool clipping; static Renderer * renderer; static RenderPipelineConfiguration renderPipelineConfiguration; static RenderLayer * renderLayer; static ShaderConfiguration2DTexture * shaderConfigurationText; static ShaderConfiguration * shaderConfigurationLine; static Renderable * textSprite, * lineSprite; static Texture * fontTexture; struct drawStringContext { Color4f color; VertexIO * vertexIO; }; static void writeVerticesGlyphCallback(BitmapFont * font, Rect4f vertexBounds, Rect4f textureBounds, void * context) { struct drawStringContext * contextStruct = context; struct vertex_p2f_t2f_c4f vertices[4], vertex = {{vertexBounds.xMin, vertexBounds.yMin}, {textureBounds.xMin, textureBounds.yMin}, {contextStruct->color.red, contextStruct->color.green, contextStruct->color.blue, contextStruct->color.alpha}}; vertices[0] = vertex; vertex.position[0] = vertexBounds.xMax; vertex.texCoords[0] = textureBounds.xMax; vertices[1] = vertex; vertex.position[1] = vertexBounds.yMax; vertex.texCoords[1] = textureBounds.yMax; vertices[2] = vertex; vertex.position[0] = vertexBounds.xMin; vertex.texCoords[0] = textureBounds.xMin; vertices[3] = vertex; uint32_t indexes[6] = {0, 1, 2, 2, 3, 0}; VertexIO_writeIndexedVertices(contextStruct->vertexIO, 4, vertices, 6, indexes); } static void drawString(const char * string, size_t length, Color4f color, float emSize, float offsetX, float offsetY, VertexIO * vertexIO) { struct drawStringContext contextStruct = {color, vertexIO}; BitmapFont_writeStringWithCallback(font, string, length, emSize, VECTOR2f(offsetX, offsetY), VECTOR2f_ZERO, 0, writeVerticesGlyphCallback, &contextStruct); } static void writeTextVertices(Renderable * self, VertexIO * vertexIO, void * context) { struct drawStringContext contextStruct = {COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), vertexIO}; unsigned int lastIndex = vertexIO->indexCount; TextFlow_writeStringWithCallback(textFlow, 0.0625f * scale, VECTOR2f(-6.0f * 0.0625f * scale, 0.75f), VECTOR2f(0.0f, 1.0f), 0, writeVerticesGlyphCallback, &contextStruct); if (clipping) { clipVerticesInsideRect(lastIndex, vertexIO->indexCount - lastIndex, clipBounds, vertexIO); } float stringWidth = BitmapFont_measureString(font, "Hello, world!", 13, 0); drawString("Hello, world!", 13, COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), 0.1f * scale, stringWidth * -0.05f * scale, 0.0f, vertexIO); stringWidth = BitmapFont_measureString(font, freeformText, strlen(freeformText), 0); drawString(freeformText, strlen(freeformText), COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), 0.0625f * scale, stringWidth * -0.03125f * scale, -0.0625f * scale, vertexIO); char indexString[32]; snprintf(indexString, 32, "%u, %s", (unsigned int) lastIndexAtPositionX, lastLeadingEdge ? "true" : "false"); stringWidth = BitmapFont_measureString(font, indexString, strlen(indexString), 0); drawString(indexString, strlen(indexString), COLOR4f(0.875f, 0.875f, 0.5f, 1.0f), 0.05f * scale, stringWidth * -0.025f * scale, 0.1f * scale, vertexIO); } static void writeLineVertices(Renderable * self, VertexIO * vertexIO, void * context) { struct vertex_p2f_c4f vertices[4]; uint32_t indexes[8]; if (clipping) { struct vertex_p2f_c4f vertex = {{clipBounds.xMin, clipBounds.yMin}, {0.5f, 0.0f, 0.0f, 1.0f}}; vertices[0] = vertex; vertex.position[0] = clipBounds.xMax; vertices[1] = vertex; vertex.position[1] = clipBounds.yMax; vertices[2] = vertex; vertex.position[0] = clipBounds.xMin; vertices[3] = vertex; indexes[0] = 0; indexes[1] = 1; indexes[2] = 1; indexes[3] = 2; indexes[4] = 2; indexes[5] = 3; indexes[6] = 3; indexes[7] = 0; VertexIO_writeIndexedVertices(vertexIO, 4, vertices, 8, indexes); } struct vertex_p2f_c4f vertex = {{(-6.0f + textFlowWidth) * 0.0625f * scale, -1.0f}, {0.5f, 0.5f, 0.0f, 1.0f}}; vertices[0] = vertex; vertex.position[1] = 1.0f; vertices[1] = vertex; indexes[0] = 0; indexes[1] = 1; VertexIO_writeIndexedVertices(vertexIO, 2, vertices, 2, indexes); if (lastPositionAtIndex.y >= 0.0f) { Vector2f screenPosition = VECTOR2f((-6.0f + lastPositionAtIndex.x) * 0.0625f * scale, 0.75f - (lastPositionAtIndex.y + 0.5f) * 0.0625f * scale); struct vertex_p2f_c4f vertex = {{0.0f, 0.0f}, {0.0f, 1.0f, 0.0f, 1.0f}}; vertex.position[0] = screenPosition.x - 0.015625f; vertex.position[1] = screenPosition.y; vertices[0] = vertex; vertex.position[0] = screenPosition.x; vertex.position[1] = screenPosition.y - 0.015625f; vertices[1] = vertex; vertex.position[0] = screenPosition.x + 0.015625f; vertex.position[1] = screenPosition.y; vertices[2] = vertex; vertex.position[0] = screenPosition.x; vertex.position[1] = screenPosition.y + 0.015625f; vertices[3] = vertex; indexes[0] = 0; indexes[1] = 1; indexes[2] = 1; indexes[3] = 2; indexes[4] = 2; indexes[5] = 3; indexes[6] = 3; indexes[7] = 0; VertexIO_writeIndexedVertices(vertexIO, 4, vertices, 8, indexes); } } static bool Target_draw(double referenceTime, double activeDrawDelta) { Renderer_clear(renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f)); Renderer_drawLayer(renderer, renderLayer, referenceTime, activeDrawDelta); AutoFreePool_empty(); return true; } static void Target_resized(unsigned int newWidth, unsigned int newHeight, double referenceTime) { viewportWidth = newWidth; viewportHeight = newHeight; viewportRatio = (float) viewportWidth / viewportHeight; if (renderer != NULL) { Renderer_setViewport(renderer, 0, 0, newWidth, newHeight, NULL); call_virtual(setProjectionMatrix, shaderConfigurationText, Matrix4x4f_ortho(MATRIX4x4f_IDENTITY, -viewportRatio, viewportRatio, -1.0f, 1.0f, -1.0f, 1.0f)); } } static void Target_keyDown(unsigned int charCode, unsigned int keyCode, unsigned int keyModifiers, bool isRepeat, double referenceTime) { if (keyCode == KEY_CODE_BACKSPACE && strlen(freeformText) > 0) { freeformText[strlen(freeformText) - 1] = '\x00'; Shell_redisplay(); } else if (keyCode == KEY_CODE_TAB) { if (keyModifiers & MODIFIER_SHIFT_BIT) { clipping = !clipping; } else { textFlow->wrapMode = (textFlow->wrapMode == WORD_WRAP_NORMAL ? WORD_WRAP_AGGRESSIVE : WORD_WRAP_NORMAL); } Shell_redisplay(); } else if (charCode >= BITMAPFONT_PRINTABLE_MIN && charCode <= BITMAPFONT_PRINTABLE_MAX && strlen(freeformText) < FREEFORM_LENGTH_MAX) { freeformText[strlen(freeformText)] = charCode; Shell_redisplay(); #if defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos) } else if (keyCode == KEYBOARD_RETURN_OR_ENTER) { EAGLShell_hideKeyboard(); #endif } } static void Target_keyModifiersChanged(unsigned int modifiers, unsigned int lastModifiers, double referenceTime) { } static void queryIndexAtPosition(float x, float y) { if (y < 0.0f) { lastIndexAtPositionX = BitmapFont_indexAtPositionX(font, freeformText, TEXT_STRLEN, 0.0625f, x, 0.5f, TEXT_OPTION_NO_TERMINATING_ADVANCE, &lastLeadingEdge); lastPositionAtIndex = VECTOR2f(0.0f, -1.0f); } else if (y < 0.1f) { lastIndexAtPositionX = BitmapFont_indexAtPositionX(font, "Hello, world!", 13, 0.1f, x, 0.5f, TEXT_OPTION_NO_TERMINATING_ADVANCE, &lastLeadingEdge); lastPositionAtIndex = VECTOR2f(0.0f, -1.0f); } else { lastIndexAtPositionX = TextFlow_indexAtPosition(textFlow, VECTOR2f((x + 6.0f * 0.0625f) / 0.0625f, -(y - 0.75f) / 0.0625f), TEXT_OPTION_NO_TERMINATING_ADVANCE, &lastLeadingEdge); lastPositionAtIndex = TextFlow_positionAtIndex(textFlow, lastIndexAtPositionX, TEXT_OPTION_NO_TERMINATING_ADVANCE); } Shell_redisplay(); } static void Target_mouseDown(unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifierFlags, double referenceTime) { Vector2f transformedPosition; transformedPosition = transformMousePosition_signedCenter(VECTOR2f(x, y), viewportWidth, viewportHeight, 1.0f); lastDragX = transformedPosition.x; lastDragY = transformedPosition.y; if (clipping && (modifierFlags & MODIFIER_CONTROL_BIT)) { if (modifierFlags & MODIFIER_SHIFT_BIT) { draggingClipBounds1 = true; } else { draggingClipBounds2 = true; } } else if (modifierFlags & MODIFIER_SHIFT_BIT) { draggingTextFlow = true; } else { queryIndexAtPosition(transformedPosition.x, transformedPosition.y); } #if defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos) if (buttonNumber > 0) { EAGLShell_showKeyboard(); } #endif } static void Target_mouseDragged(unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifierFlags, double referenceTime) { Vector2f transformedPosition; transformedPosition = transformMousePosition_signedCenter(VECTOR2f(x, y), viewportWidth, viewportHeight, 1.0f); if (draggingTextFlow) { textFlowWidth += (transformedPosition.x - lastDragX) / (0.0625f * scale); textFlow->wrapWidth = textFlowWidth; Shell_redisplay(); } else if (draggingClipBounds1) { clipBounds.xMin += transformedPosition.x - lastDragX; clipBounds.yMin += transformedPosition.y - lastDragY; Shell_redisplay(); } else if (draggingClipBounds2) { clipBounds.xMax += transformedPosition.x - lastDragX; clipBounds.yMax += transformedPosition.y - lastDragY; Shell_redisplay(); } else { queryIndexAtPosition(transformedPosition.x, transformedPosition.y); } lastDragX = transformedPosition.x; lastDragY = transformedPosition.y; } static void Target_mouseUp(unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifierFlags, double referenceTime) { draggingTextFlow = false; draggingClipBounds1 = false; draggingClipBounds2 = false; } static void registerShellCallbacks() { Shell_drawFunc(Target_draw); Shell_resizeFunc(Target_resized); Shell_keyDownFunc(Target_keyDown); Shell_keyModifiersChangedFunc(Target_keyModifiersChanged); Shell_mouseDownFunc(Target_mouseDown); Shell_mouseDraggedFunc(Target_mouseDragged); Shell_mouseUpFunc(Target_mouseUp); } #ifndef STEM_PLATFORM_android static void printUsage() { fprintf(stderr, "Usage: font_testharness [-json /path/to/font.json] [-scale %%f]\n"); } static void parseArgs(int argc, const char ** argv) { int argIndex; bool printUsageAfterParsing = false; for (argIndex = 1; argIndex < argc; argIndex++) { if (!strcmp(argv[argIndex], "--help")) { printUsageAfterParsing = true; } else if (!strcmp(argv[argIndex], "-json")) { argIndex++; if (argIndex >= argc) { printf("-json specified as last argument; must be followed by a file path\n"); printUsageAfterParsing = true; break; } jsonPath = argv[argIndex]; } else if (!strcmp(argv[argIndex], "-scale")) { argIndex++; if (argIndex >= argc) { printf("-scale specified as last argument; must be followed by a number\n"); printUsageAfterParsing = true; break; } if (!sscanf(argv[argIndex], "%f", &scale)) { printf("Couldn't parse value specified to -scale (%s) as a number\n", argv[argIndex]); printUsageAfterParsing = true; break; } } else { printf("Unrecognized argument: %s\n", argv[argIndex]); printUsageAfterParsing = true; } } if (printUsageAfterParsing) { printUsage(); } } #endif #if defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos) void EAGLTarget_configure(int argc, char ** argv, struct EAGLShellConfiguration * configuration) { parseArgs(argc, (const char **) argv); configuration->preferredOpenGLAPIVersion = EAGLShellOpenGLVersion_ES1 | EAGLShellOpenGLVersion_ES2; #elif defined(STEM_PLATFORM_macosx) void NSOpenGLTarget_configure(int argc, const char ** argv, struct NSOpenGLShellConfiguration * configuration) { parseArgs(argc, argv); configuration->windowTitle = "Font Test Harness"; configuration->useGLCoreProfile = true; #elif defined(STEM_PLATFORM_windows) void WGLTarget_configure(void * instance, void * prevInstance, char * commandLine, int command, int argc, const char ** argv, struct WGLShellConfiguration * configuration) { parseArgs(argc, argv); configuration->windowTitle = "Font Test Harness"; configuration->useGLCoreProfile = true; #elif defined(STEM_PLATFORM_linux) void GLXTarget_configure(int argc, const char ** argv, struct GLXShellConfiguration * configuration) { parseArgs(argc, argv); #include "IconData_stemapp.h" configuration->icon.data = STATIC_iconData_stemapp; configuration->icon.size = sizeof(STATIC_iconData_stemapp) / sizeof(STATIC_iconData_stemapp[0]); configuration->applicationName = STEM_HUMAN_READABLE_TARGET_NAME; configuration->windowTitle = "Font Test Harness"; #elif defined(STEM_PLATFORM_android) void EGLTarget_configure(struct EGLShellConfiguration * configuration) { readResourceFile = EGLShell_readResourceFile; #else #error Unsupported platform #endif registerShellCallbacks(); } void Target_init() { renderer = Renderer_create(); renderPipelineConfiguration = default2DRenderPipelineConfiguration(RENDER_BLEND_ALPHA); renderLayer = RenderLayer_create(RENDER_LAYER_SORT_NONE, NULL, NULL); JSONDeserializationContext * context; const char * fontJSONFilePath; if (jsonPath == NULL) { chdir(Shell_getResourcePath()); fontJSONFilePath = "test_font.json"; context = JSONDeserializationContext_createWithResourceFile(fontJSONFilePath); } else { fontJSONFilePath = jsonPath; context = JSONDeserializationContext_createWithFile(fontJSONFilePath); } if (context->status != SERIALIZATION_ERROR_OK) { fprintf(stderr, "Fatal error: Couldn't load %s (status %d)\n", fontJSONFilePath, context->status); exit(EXIT_FAILURE); } font = BitmapFont_deserialize(context); call_virtual(dispose, context); if (font == NULL) { fprintf(stderr, "Fatal error: Couldn't deserialize %s (status %d)\n", fontJSONFilePath, context->status); exit(EXIT_FAILURE); } char atlasJSONFilePath[PATH_MAX]; if (jsonPath == NULL) { strncpy_safe(atlasJSONFilePath, font->atlasName, sizeof(atlasJSONFilePath)); context = JSONDeserializationContext_createWithResourceFile(atlasJSONFilePath); } else { size_t charIndex; strncpy_safe(atlasJSONFilePath, jsonPath, sizeof(atlasJSONFilePath)); for (charIndex = strlen(atlasJSONFilePath) - 1; charIndex > 0; charIndex--) { if (atlasJSONFilePath[charIndex] == '/') { charIndex++; break; } } strncpy_safe(atlasJSONFilePath + charIndex, font->atlasName, sizeof(atlasJSONFilePath) - charIndex); context = JSONDeserializationContext_createWithFile(atlasJSONFilePath); } if (context->status != SERIALIZATION_ERROR_OK) { fprintf(stderr, "Fatal error: Couldn't load %s (status %d)\n", atlasJSONFilePath, context->status); exit(EXIT_FAILURE); } TextureAtlasData * atlasData = TextureAtlasData_deserialize(context); call_virtual(dispose, context); if (atlasData == NULL) { fprintf(stderr, "Fatal error: Couldn't deserialize %s (status %d)\n", atlasJSONFilePath, context->status); exit(EXIT_FAILURE); } BitmapImage * image; char textureImageFilePath[PATH_MAX]; if (jsonPath == NULL) { strncpy_safe(textureImageFilePath, atlasData->textureName, sizeof(textureImageFilePath)); image = ImageIO_readImageResourceFile(textureImageFilePath, BITMAP_PIXEL_FORMAT_RGBA_8888, true); } else { size_t charIndex; strncpy_safe(textureImageFilePath, jsonPath, sizeof(textureImageFilePath)); for (charIndex = strlen(textureImageFilePath) - 1; charIndex > 0; charIndex--) { if (textureImageFilePath[charIndex] == '/') { charIndex++; break; } } strncpy_safe(textureImageFilePath + charIndex, atlasData->textureName, sizeof(textureImageFilePath) - charIndex); image = ImageIO_readImageFile(textureImageFilePath, BITMAP_PIXEL_FORMAT_RGBA_8888, true); } if (image == NULL) { fprintf(stderr, "Fatal error: Couldn't load %s\n", textureImageFilePath); exit(EXIT_FAILURE); } TextureAtlas * atlas = TextureAtlasData_createTextureAtlas(atlasData, image->width, image->height); fontTexture = Texture_createWithTexels2D(image->pixels, image->width, image->height, 4, TEXEL_COMPONENT_UINT8_NORM, TEXEL_SWIZZLE_DEFAULT, 0); call_virtual(dispose, image); BitmapFont_readAtlasEntries(font, atlas); TextureAtlasData_dispose(atlasData); textFlowWidth = 12.0f; clipBounds = RECT4f(-6.0f * 0.0625f, 6.0f * 0.0625f, 0.25f, 0.75f); textFlow = TextFlow_create(font, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut eleifend commodo placerat. Aenean a viverra leo.", false, false, ALIGN_LEFT, WORD_WRAP_NORMAL, textFlowWidth); shaderConfigurationText = ShaderConfiguration2DTexture_create(ShaderCollection_get2DTextureShader()); call_virtual(setTexture, shaderConfigurationText, 0, fontTexture, false); call_virtual(setProjectionMatrix, shaderConfigurationText, Matrix4x4f_ortho(MATRIX4x4f_IDENTITY, -viewportRatio, viewportRatio, -1.0f, 1.0f, -1.0f, 1.0f)); shaderConfigurationLine = ShaderConfiguration_create(ShaderCollection_get2DColorShader()); call_virtual(referenceUniformFromConfiguration, shaderConfigurationLine, ATOM("projectionTransform"), shaderConfigurationText, ATOM("projectionTransform")); textSprite = Renderable_createWithCallback(PRIMITIVE_TRIANGLES, &renderPipelineConfiguration, shaderConfigurationText, writeTextVertices, NULL, NULL); lineSprite = Renderable_createWithCallback(PRIMITIVE_LINES, &renderPipelineConfiguration, shaderConfigurationLine, writeLineVertices, NULL, NULL); RenderLayer_addRenderable(renderLayer, textSprite, 0, RECT4i_EMPTY); RenderLayer_addRenderable(renderLayer, lineSprite, 0, RECT4i_EMPTY); memset(freeformText, 0, FREEFORM_LENGTH_MAX + 1); Shell_mainLoop(); }