From 06f784d0b11200fff1eb660b7d235e0e111c532e Mon Sep 17 00:00:00 2001 From: Daniel Ledda Date: Sat, 21 Feb 2026 12:47:32 +0100 Subject: [PATCH] getting text rendering working with stb_truetype --- assets/fonts/KodeMono.ttf | Bin 0 -> 59968 bytes assets/shaders/text.fragment.glsl | 11 ++ assets/shaders/text.vertex.glsl | 73 ++++++++++ build | 2 +- src/common.h | 12 ++ src/gfx/Mesh.c | 16 +-- src/gfx/Shader.c | 7 + src/gfx/Shader.h | 3 + src/gfx/Texture.c | 87 ++++++++++- src/gfx/Texture.h | 45 +++++- src/main.c | 148 ++++++++++--------- src/render.c | 231 +++++++++++++++++++++--------- src/render.h | 80 +++++++---- src/ui.c | 131 ++++++++++------- src/ui.h | 28 +++- 15 files changed, 637 insertions(+), 237 deletions(-) create mode 100644 assets/fonts/KodeMono.ttf create mode 100644 assets/shaders/text.fragment.glsl create mode 100644 assets/shaders/text.vertex.glsl create mode 100644 src/common.h diff --git a/assets/fonts/KodeMono.ttf b/assets/fonts/KodeMono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..07da5896483095154a3cd9012d43ec86bfcf6e56 GIT binary patch literal 59968 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࿈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 z zrGeQ)<}7EeIBJv~`fb`-SFo^1CL>nExSfK2_1mE=roeOi=xBy3t>Qg>bhL+qC4t2) z)dc1n&N({@TY`X1ZCWg_7;MIb5`q@>YRV_ud+hcOyJNn@@#V zgh+i!@x8s4XhR@+LlJV8)Rj``+={iFJ8b*IcaFaEp_{U-O&t!vzQf@I2hOPx|CUpO zb8S%Go@A{uIA~U-L`5kS+!7czTW$1^4eZQ_i=EeXY+nwRpPZRHx5PRJ!_xnk93}s; zJ|8)6pR@JWNGLRta@JeaPbEwLw*X6>+5XexmPi|!=X2ZI=J0{`_RjHs`p=RJ9V=@! W!f2GZ{Bqi1yP(U~v53$9Q~wuq_>U+6 literal 0 HcmV?d00001 diff --git a/assets/shaders/text.fragment.glsl b/assets/shaders/text.fragment.glsl new file mode 100644 index 0000000..7bb77e9 --- /dev/null +++ b/assets/shaders/text.fragment.glsl @@ -0,0 +1,11 @@ +#version 330 core +out vec4 pixel_color; + +in vec4 frag_color; +in vec2 frag_uv_position; + +uniform sampler2D glyph_atlas; + +void main() { + pixel_color = vec4(1,1,1,texture(glyph_atlas, frag_uv_position).r); +}; diff --git a/assets/shaders/text.vertex.glsl b/assets/shaders/text.vertex.glsl new file mode 100644 index 0000000..6a344eb --- /dev/null +++ b/assets/shaders/text.vertex.glsl @@ -0,0 +1,73 @@ +#version 330 core +layout (location = 0) in vec2 begin; +layout (location = 1) in int glyph; +layout (location = 2) in float lineHeight; +layout (location = 3) in vec4 color; + +/* +typedef struct GlyphMeta GlyphMeta; +struct GlyphMeta { + // chunk 1 + RLVector2 uv0; + RLVector2 uv1; + + // chunk 2 + real32 xOffset; + real32 yOffset; + real32 width; + real32 height; +}; +*/ + +uniform samplerBuffer glyph_table; + +uniform mat4 projection; +uniform sampler2D font; + +out vec4 frag_color; +out vec2 frag_uv_position; + +const vec2 rectangle_vertices[4] = vec2[]( + vec2(-1, -1), // bl + vec2(-1, 1), // tl + vec2( 1, -1), // br + vec2( 1, 1) // tr +); + +const vec2 uv0_vertices[4] = vec2[]( + vec2(1, 0), // bl + vec2(1, 1), // tl + vec2(0, 0), // br + vec2(0, 1) // tr +); + +const vec2 uv1_vertices[4] = vec2[]( + vec2(0, 1), + vec2(0, 0), + vec2(1, 1), + vec2(1, 0) +); + +void main() { + vec4 chunk1 = texelFetch(glyph_table, glyph * 2 + 0); + vec4 chunk2 = texelFetch(glyph_table, glyph * 2 + 1); + + vec2 uv0 = chunk1.xy; + vec2 uv1 = chunk1.zw; + vec2 offset = chunk2.xy; + vec2 dims = chunk2.zw; + + vec2 p0 = begin + offset*lineHeight; + vec2 p1 = begin + (offset + dims)*lineHeight; + + vec2 dest_half_size = (p1 - p0) / 2; + vec2 dest_center = (p1 + p0) / 2; + vec2 dest_position = rectangle_vertices[gl_VertexID] * dest_half_size + dest_center; + + vec2 uv_position = uv0 * uv0_vertices[gl_VertexID] + uv1 * uv1_vertices[gl_VertexID]; + + gl_Position = projection * vec4(dest_position, 0, 1); + + frag_color = color; + frag_uv_position = vec2(uv_position.x, uv_position.y); +} diff --git a/build b/build index 4fbd9f8..e2bd750 100755 --- a/build +++ b/build @@ -7,7 +7,7 @@ echo [Building target] if [ $DEBUG ]; then time clang -O0 -g -g2 $COMMON_FLAGS -DDJSTDLIB_DEBUG=1 ./src/main.c -o ./target/somaesque $LIB_INCLUDE else - time clang -O3 $COMMON_FLAGS ./src/main.c -o ./target/somaesque $LIB_INCLUDE + time clang -O2 $COMMON_FLAGS ./src/main.c -o ./target/somaesque $LIB_INCLUDE fi echo [Target built] diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..1abe185 --- /dev/null +++ b/src/common.h @@ -0,0 +1,12 @@ +#ifndef COMMON_H +#define COMMON_H + +#include "lib/djstdlib/core.h" +#include "lib/raymath.h" + +DefineList(RLVector2, RLVec2); +DefineList(RLVector4, RLVec4); +DefineList(real32, Float); +DefineList(uint32, UInt32); + +#endif diff --git a/src/gfx/Mesh.c b/src/gfx/Mesh.c index 0117b5a..473c454 100644 --- a/src/gfx/Mesh.c +++ b/src/gfx/Mesh.c @@ -23,14 +23,14 @@ Mesh createMesh(const char* obj_file) { size_t num_materials; int success = tinyobj_parse_obj( - &attrib, - &shapes, - &num_shapes, - &materials, - &num_materials, - obj_file, - tinyobj_get_filedata, - (void *)temp.arena, + &attrib, + &shapes, + &num_shapes, + &materials, + &num_materials, + obj_file, + tinyobj_get_filedata, + (void *)temp.arena, TINYOBJ_FLAG_TRIANGULATE ); diff --git a/src/gfx/Shader.c b/src/gfx/Shader.c index bfcc3ec..5d40cbf 100644 --- a/src/gfx/Shader.c +++ b/src/gfx/Shader.c @@ -83,6 +83,13 @@ void setUniform3fvByLoc(int uniformLocation, RLVector3 *vector) { glUniform3fv(uniformLocation, 1, Vector3ToFloat(*vector)); } +void setUniform1i(Shader *s, const char *uniformName, int32 i) { + glUniform1i(glGetUniformLocation(s->progId, uniformName), (GLint)i); +} +void setUniform1iByLoc(int uniformLocation, int32 i) { + glUniform1i(uniformLocation, (GLint)i); +} + void setUniform2fv(Shader *s, const char *uniformName, RLVector2 *vector) { glUniform2fv(glGetUniformLocation(s->progId, uniformName), 1, (const GLfloat *)vector); } diff --git a/src/gfx/Shader.h b/src/gfx/Shader.h index d471c1a..4bbb20d 100644 --- a/src/gfx/Shader.h +++ b/src/gfx/Shader.h @@ -22,6 +22,9 @@ void setUniform3fvByLoc(int uniformLocation, RLVector3 *vector); void setUniform2fv(Shader *s, const char *uniformName, RLVector2 *vector); void setUniform2fvByLoc(int uniformLocation, RLVector2 *vector); +void setUniform1i(Shader *s, const char *uniformName, int32 i); +void setUniform1iByLoc(int uniformLocation, int32 i); + int getUniformLocation(Shader *s, const char *uniformName); #endif diff --git a/src/gfx/Texture.c b/src/gfx/Texture.c index d9444b4..d4fec4b 100644 --- a/src/gfx/Texture.c +++ b/src/gfx/Texture.c @@ -1,9 +1,88 @@ #include "Texture.h" -#include "../lib/loaders/stb_image.h" -#include "../lib/glad/glad.h" -#include "../lib/djstdlib/core.h" -Texture createTexture(const char* source_path) { +Texture createTexture(const char* bitmap, int32 width, int32 height) { + Texture result = {0}; + result.width = width; + result.height = height; + glGenTextures(1, &result.tex_id); + glBindTexture(GL_TEXTURE_2D, result.tex_id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, result.width, result.height, 0, GL_RGB, GL_UNSIGNED_BYTE, bitmap); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glGenerateMipmap(GL_TEXTURE_2D); + + return result; +} + +DefineList(stbtt_bakedchar, STBBakedChar); + +Font createFont(Arena *arena, string ttfLocation, real32 lineHeight) { + const int CODEPOINT_START = 32; + const int CODEPOINT_END = 126; + + int32 atlasWidth = 16 * lineHeight; + int32 atlasHeight = 6 * lineHeight; + + STBBakedCharList bakedCharlist = PushFullListZero(arena, STBBakedCharList, CODEPOINT_END - CODEPOINT_START); + string fontFile = os_readEntireFile(arena, ttfLocation); + + char *bakedFontBitmap = PushArrayZero(arena, char, atlasWidth*atlasHeight); + int32 bake_result = stbtt_BakeFontBitmap( + (unsigned char *)fontFile.str, 0, + lineHeight, + (unsigned char *)bakedFontBitmap, atlasWidth, atlasHeight, + CODEPOINT_START, CODEPOINT_END - CODEPOINT_START, + bakedCharlist.data); + + GlyphMetaList glyphMeta = PushFullListZero(arena, GlyphMetaList, bakedCharlist.length); + real32 x, y; + real32 lastX, lastY; + for (EachIn(bakedCharlist, i)) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(bakedCharlist.data, atlasWidth, atlasHeight, i, &x, &y, &q, 1); + glyphMeta.data[i] = (GlyphMeta){ + .uv0=(RLVector2){q.s0, q.t1}, + .uv1=(RLVector2){q.s1, q.t0}, + .xOffset=q.x0 - lastX, + .yOffset=q.y0 + lineHeight, + .width=q.x1-q.x0, + .height=q.y1-q.y0, + }; + lastX = x; + lastY = y; + } + + Font result = {0}; + result.glyphMeta = glyphMeta; + result.lineHeight = lineHeight; + result.charWidth = bakedCharlist.data[0].xadvance; + + glGenTextures(1, &result.texId); + glBindTexture(GL_TEXTURE_2D, result.texId); + //glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, atlasWidth, atlasHeight, 0, GL_RED, GL_UNSIGNED_BYTE, bakedFontBitmap); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glGenerateMipmap(GL_TEXTURE_2D); + + glGenBuffers(1, &result.glyphTableBufId); + glBindBuffer(GL_TEXTURE_BUFFER, result.glyphTableBufId); + glBufferData(GL_TEXTURE_BUFFER, sizeof(result.glyphMeta.data[0])*result.glyphMeta.length, result.glyphMeta.data, GL_STATIC_DRAW); + + glGenTextures(1, &result.glyphTableTexId); + glBindTexture(GL_TEXTURE_BUFFER, result.glyphTableTexId); + glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, result.glyphTableBufId); + + return result; +} + +Texture createTextureFromFile(const char* source_path) { Texture result = {0}; glGenTextures(1, &result.tex_id); glBindTexture(GL_TEXTURE_2D, result.tex_id); diff --git a/src/gfx/Texture.h b/src/gfx/Texture.h index 153af12..c8fdc41 100644 --- a/src/gfx/Texture.h +++ b/src/gfx/Texture.h @@ -1,12 +1,45 @@ #ifndef LEDDA_TEXTURE_H #define LEDDA_TEXTURE_H -typedef struct { - unsigned int tex_id; - int width; - int height; -} Texture; +#include "../lib/djstdlib/core.h" +#include "../lib/loaders/stb_truetype.h" +#include "../lib/loaders/stb_image.h" +#include "../lib/glad/glad.h" +#include "../lib/djstdlib/core.h" +#include "../lib/djstdlib/os.h" +#include "../common.h" -Texture createTexture(const char* source_path); +typedef struct Texture Texture; +struct Texture { + uint32 tex_id; + int32 width; + int32 height; +}; + +typedef struct GlyphMeta GlyphMeta; +struct GlyphMeta { + RLVector2 uv0; + RLVector2 uv1; + real32 xOffset; + real32 yOffset; + real32 width; + real32 height; +}; + +DefineList(GlyphMeta, GlyphMeta); + +typedef struct Font Font; +struct Font { + GlyphMetaList glyphMeta; + real32 lineHeight; + real32 charWidth; + uint32 texId; + uint32 glyphTableTexId; + uint32 glyphTableBufId; +}; + +Texture createTexture(const char* bitmap, int32 width, int32 height); +Texture createTextureFromFile(const char* source_path); +Font createFont(Arena *arena, string ttfLocation, real32 lineHeight); #endif diff --git a/src/main.c b/src/main.c index ce7041d..d92dfc8 100644 --- a/src/main.c +++ b/src/main.c @@ -1,10 +1,7 @@ -#include "lib/djstdlib/core.h" -#define _POSIX_C_SOURCE 199309L -#include "time.h" - // Library initialisation #define RAYMATH_IMPLEMENTATION #define STB_IMAGE_IMPLEMENTATION +#define STB_TRUETYPE_IMPLEMENTATION #define TINYOBJ_LOADER_C_IMPLEMENTATION // Project @@ -67,6 +64,7 @@ struct SomaState { RLVector3 rotAxisX; RLVector3 rotAxisY; UI_Rect *threedeePaneRect; + real64 explosionOffset; }; typedef struct Soma Soma; @@ -209,17 +207,12 @@ static void advanceDisplayReverse(Soma *soma) { static void advanceDisplay(Soma *soma) { if (soma->state.displayingSolutions) { - if (soma->state.displayedSolution == soma->solutions.length - 1) { - soma->state.displayedSolution = 0; - } else { - soma->state.displayedSolution += 1; - } + soma->state.explosionOffset = 0; + soma->state.displayedSolution += 1; + soma->state.displayedSolution %= soma->solutions.length; } else { - if (soma->state.displayedPolycube == soma->polycubeInput.length - 1) { - soma->state.displayedPolycube = 0; - } else { - soma->state.displayedPolycube += 1; - } + soma->state.displayedPolycube += 1; + soma->state.displayedPolycube %= soma->polycubes.length; } } @@ -281,6 +274,11 @@ void processInput(Soma *soma) { if (soma->state.displayingSolutions) { real64 scrollDelta = soma->ui->prevInput->mouse.scroll.dY; if (scrollDelta > 0.001 || scrollDelta < -0.001) { + soma->state.explosionOffset += scrollDelta; + if (soma->state.explosionOffset > 0) { + scrollDelta -= soma->state.explosionOffset; + soma->state.explosionOffset = 0; + } SceneGraphNode *rootNode = getSceneGraphNode(soma->scene, soma->solutionNode); int32 nextChildHandle = rootNode->firstChild; while (nextChildHandle) { @@ -335,45 +333,47 @@ static void ui_Interaction(UI_Context *ui, Soma *soma) { real32 padding = 20; real32 childGap = 5; - UI(ui, .childGap=padding, .flags=UI_Flag_Vertical) { - if (soma->state.displayingSolutions && soma->solutions.length > 0) { - SomaSolution *currentSolution = &soma->solutions.data[soma->state.displayedSolution]; - for (int x = 0; x < soma->puzzleDims[0]; x++) UI(ui, .childGap=childGap, .flags=UI_Flag_Vertical) { - for (int y = 0; y < soma->puzzleDims[1]; y++) UI(ui, .childGap=childGap) { - for (int z = 0; z < soma->puzzleDims[2]; z++) { - for (EachIn(*currentSolution, i)) { - uint64 spaceRepr = currentSolution->data[i]; - bool cellActive = filledAt(&(VoxelSpace){ - .space = spaceRepr, - .dim_x = soma->puzzleDims[0], - .dim_y = soma->puzzleDims[1], - .dim_z = soma->puzzleDims[2], - }, x, y, z); - if (cellActive) UI(ui, - .width=boxSize, - .height=boxSize, - .color=soma->polycubeInput.data[i].color, - .borderRadius=5, - .borderThickness=0, - ); + UI(.flags=UI_Flag_Center | UI_Flag_HeightGrow) { + UI(.childGap=padding, .flags=UI_Flag_Vertical | UI_Flag_Center) { + if (soma->state.displayingSolutions && soma->solutions.length > 0) { + SomaSolution *currentSolution = &soma->solutions.data[soma->state.displayedSolution]; + for (int x = 0; x < soma->puzzleDims[0]; x++) UI(.childGap=childGap, .flags=UI_Flag_Vertical) { + for (int y = 0; y < soma->puzzleDims[1]; y++) UI(.childGap=childGap) { + for (int z = 0; z < soma->puzzleDims[2]; z++) { + for (EachIn(*currentSolution, i)) { + uint64 spaceRepr = currentSolution->data[i]; + bool cellActive = filledAt(&(VoxelSpace){ + .space = spaceRepr, + .dim_x = soma->puzzleDims[0], + .dim_y = soma->puzzleDims[1], + .dim_z = soma->puzzleDims[2], + }, x, y, z); + if (cellActive) UI( + .width=boxSize, + .height=boxSize, + .color=soma->polycubeInput.data[i].color, + .borderRadius=5, + .borderThickness=0, + ); + } } } } - } - } else { - PolycubeInput *currentPolycube = &soma->polycubeInput.data[soma->state.displayedPolycube]; - for (int x = 0; x < currentPolycube->repr.dim_x; x++) UI(ui, .childGap=childGap, .flags=UI_Flag_Vertical) { - for (int y = 0; y < currentPolycube->repr.dim_y; y++) UI(ui, .childGap=childGap) { - for (int z = 0; z < currentPolycube->repr.dim_z; z++) { - bool cellActive = filledAt(¤tPolycube->repr, x, y, z); - if (ui_CheckboxRect(ui, &cellActive, UI_CreateRect( - .width = boxSize, - .height = boxSize, - .borderRadius = 2, - .color = currentPolycube->color, - ))) { - soma->state.polycubeDirty = true; - spaceSet(¤tPolycube->repr, cellActive, x, y, z); + } else { + PolycubeInput *currentPolycube = &soma->polycubeInput.data[soma->state.displayedPolycube]; + for (int x = 0; x < currentPolycube->repr.dim_x; x++) UI(.childGap=childGap, .flags=UI_Flag_Vertical) { + for (int y = 0; y < currentPolycube->repr.dim_y; y++) UI(.childGap=childGap) { + for (int z = 0; z < currentPolycube->repr.dim_z; z++) { + bool cellActive = filledAt(¤tPolycube->repr, x, y, z); + if (ui_CheckboxRect(ui, &cellActive, UI_CreateRect( + .width = boxSize, + .height = boxSize, + .borderRadius = 2, + .color = currentPolycube->color, + ))) { + soma->state.polycubeDirty = true; + spaceSet(¤tPolycube->repr, cellActive, x, y, z); + } } } } @@ -383,39 +383,41 @@ static void ui_Interaction(UI_Context *ui, Soma *soma) { } static void DJUI_Soma(UI_Context *ui, Soma *soma) { - UI(ui, .flags=UI_Flag_HeightGrow | UI_Flag_WidthGrow) { - UI(ui, .padding=UI_PadUniform(10), .color={0.2, 0.2, 0.2, 1}, .flags=UI_Flag_HeightGrow) { + RLVector4 darkgrey = {0.2, 0.2, 0.2, 1}; + RLVector4 grey = {0.4, 0.4, 0.4, 1}; + + UI(.flags=UI_Flag_HeightGrow | UI_Flag_WidthGrow) { + UI(.padding={.left=20, .right=20, .top=5}, .color=darkgrey, .flags=UI_Flag_HeightGrow | UI_Flag_Vertical) { + UI() UI_Text(s("Somaesque"), .lineHeight=26); ui_Interaction(ui, soma); } - UI(ui, .flags=UI_Flag_Vertical | UI_Flag_HeightGrow | UI_Flag_WidthGrow) { - UI(ui, .padding=UI_PadUniform(5), .color={0.2, 0.2, 0.2, 1}, .flags=UI_Flag_WidthGrow) { - UI(ui, .width=65); - UI(ui, .flags=UI_Flag_WidthGrow); - UI(ui) { - RLVector4 color = {0.5, 0.5, 0.5, 1}; - UI(ui, .childGap=10) { - if (ui_Button(ui, UI_CreateRect(.width=30, .height=30, .color=color))) { + UI(.flags=UI_Flag_Vertical | UI_Flag_HeightGrow | UI_Flag_WidthGrow) { + UI(.padding=UI_PadUniform(10), .color=darkgrey, .flags=UI_Flag_WidthGrow) { + UI(.flags=UI_Flag_WidthGrow) { + UI(.childGap=10) { + if (ui_Button(ui, UI_CreateRect(.height=30, .padding={.left=10, .right=10}, .color=grey), UI_CreateText(s("Previous"), .lineHeight=20))) { advanceDisplayReverse(soma); } - if (ui_Button(ui, UI_CreateRect(.width=30, .height=30, .color=color))) { + if (ui_Button(ui, UI_CreateRect(.height=30, .padding={.left=10, .right=10}, .color=grey), UI_CreateText(s("Next"), .lineHeight=20))) { advanceDisplay(soma); } } } - UI(ui, .flags=UI_Flag_WidthGrow); - UI(ui, .width=65, .childGap=5) { + UI(.childGap=10) { if (ui_Button(ui, UI_CreateRect( .width=30, .height=30, - .color=soma->state.displayingSolutions ? (RLVector4){1,0,0,0.5} : COLOR_RED, - ))) { + .padding={.left=10, .right=10}, + .color=soma->state.displayingSolutions ? grey : (RLVector4){0.2, 0.2, 0.7, 1}, + ), UI_CreateText(s("Design"), .lineHeight=20))) { soma->state.displayingSolutions = false; } if (ui_Button(ui, UI_CreateRect( .width=30, .height=30, - .color=soma->state.displayingSolutions ? COLOR_GREEN : (RLVector4){0,1,0,0.5}, - ))) { + .padding={.left=10, .right=10}, + .color=soma->state.displayingSolutions ? (RLVector4){0.2, 0.2, 0.7, 1} : grey, + ), UI_CreateText(s("Solve"), .lineHeight=20))) { if (!soma->state.displayingSolutions) { tryScheduleSolve(soma); } else { @@ -424,7 +426,7 @@ static void DJUI_Soma(UI_Context *ui, Soma *soma) { } } } - UI(ui, .flags=UI_Flag_WidthGrow | UI_Flag_HeightGrow | UI_Flag_3DScene); + UI(.flags=UI_Flag_WidthGrow | UI_Flag_HeightGrow | UI_Flag_3DScene); } } } @@ -526,7 +528,8 @@ int mainGfx() { renderer.phongShader = &phongShader; renderer.cubeMesh = &cubeMesh; - UI_Context ui = ui_initContext(arena, &renderer); + Arena *uiArena = arenaAlloc(Megabytes(64)); + UI_Context ui = ui_initContext(uiArena, &renderer); Soma soma = { .window = { @@ -591,6 +594,13 @@ int mainGfx() { getSceneGraphNode(&mainScene, renderer.light)->translation = (RLVector3){4.0f, 6.0f, 24.0f}; + Font kodeMono = createFont(arena, s("./assets/fonts/KodeMono.ttf"), 96.0f); + renderer.activeFont = &kodeMono; + + Shader textShader = createShader(s("./assets/shaders/text.vertex.glsl"), s("./assets/shaders/text.fragment.glsl")); + renderer.textShader = &textShader; + + // Render loop real64 lastFrame = glfwGetTime(); real64 timeDelta = 1.0f / TARGET_FPS; real64 frameStart = lastFrame; diff --git a/src/render.c b/src/render.c index 40ea63a..ca4ddae 100644 --- a/src/render.c +++ b/src/render.c @@ -1,87 +1,144 @@ #include "render.h" #include "debug.h" -RenderObjects_Rectangle createRectangleObjects(Arena *arena, size_t count) { - RenderObjects_Rectangle result = {0}; +static RenderObjects_Char createCharObjects(Arena *arena, size_t count) { + RenderObjects_Char result = {0}; result.count = count; - result.p0 = PushFullList(arena, RLVec2List, count); - result.p1 = PushFullList(arena, RLVec2List, count); - result.color = PushFullList(arena, RLVec4List, count); - result.borderRadius = PushFullList(arena, FloatList, count); - result.borderThickness = PushFullList(arena, FloatList, count); - result.edgeSoftness = PushFullList(arena, FloatList, count); + result.begin.buf = PushFullList(arena, RLVec2List, count); + result.glyph.buf = PushFullList(arena, IntList, count); + result.lineHeight.buf = PushFullList(arena, FloatList, count); + result.color.buf = PushFullList(arena, RLVec4List, count); glGenVertexArrays(1, &result.vao); - glGenBuffers(1, &result.p0BufferId); - glGenBuffers(1, &result.p1BufferId); - glGenBuffers(1, &result.colorBufferId); - glGenBuffers(1, &result.borderRadiusBufferId); - glGenBuffers(1, &result.borderThicknessBufferId); - glGenBuffers(1, &result.edgeSoftnessBufferId); + glGenBuffers(1, &result.begin.bufId); + glGenBuffers(1, &result.glyph.bufId); + glGenBuffers(1, &result.lineHeight.bufId); + glGenBuffers(1, &result.color.bufId); glBindVertexArray(result.vao); - glBindBuffer(GL_ARRAY_BUFFER, result.p0BufferId); - glBufferData(GL_ARRAY_BUFFER, result.p0.length * sizeof(RLVec2List_underlying), 0, GL_DYNAMIC_DRAW); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(RLVec2List_underlying), NULL); + int32 bufItemSize = sizeof(result.begin.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.begin.bufId); + glBufferData(GL_ARRAY_BUFFER, result.begin.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, bufItemSize, NULL); glVertexAttribDivisor(0, 1); glEnableVertexAttribArray(0); - glBindBuffer(GL_ARRAY_BUFFER, result.p1BufferId); - glBufferData(GL_ARRAY_BUFFER, result.p1.length * sizeof(RLVec2List_underlying), 0, GL_DYNAMIC_DRAW); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(RLVec2List_underlying), NULL); + bufItemSize = sizeof(result.glyph.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.glyph.bufId); + glBufferData(GL_ARRAY_BUFFER, result.glyph.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribIPointer(1, 1, GL_INT, bufItemSize, NULL); glVertexAttribDivisor(1, 1); glEnableVertexAttribArray(1); - glBindBuffer(GL_ARRAY_BUFFER, result.colorBufferId); - glBufferData(GL_ARRAY_BUFFER, result.color.length * sizeof(RLVec4List_underlying), 0, GL_DYNAMIC_DRAW); - glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(RLVec4List_underlying), NULL); + bufItemSize = sizeof(result.lineHeight.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.lineHeight.bufId); + glBufferData(GL_ARRAY_BUFFER, result.lineHeight.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, bufItemSize, NULL); glVertexAttribDivisor(2, 1); glEnableVertexAttribArray(2); - glBindBuffer(GL_ARRAY_BUFFER, result.borderRadiusBufferId); - glBufferData(GL_ARRAY_BUFFER, result.borderRadius.length * sizeof(FloatList_underlying), 0, GL_DYNAMIC_DRAW); - glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, sizeof(FloatList_underlying), NULL); + bufItemSize = sizeof(result.color.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.color.bufId); + glBufferData(GL_ARRAY_BUFFER, result.color.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, bufItemSize, NULL); glVertexAttribDivisor(3, 1); glEnableVertexAttribArray(3); - glBindBuffer(GL_ARRAY_BUFFER, result.borderThicknessBufferId); - glBufferData(GL_ARRAY_BUFFER, result.borderThickness.length * sizeof(FloatList_underlying), 0, GL_DYNAMIC_DRAW); - glVertexAttribPointer(4, 1, GL_FLOAT, GL_FALSE, sizeof(FloatList_underlying), NULL); + return result; +} + +static RenderObjects_Rect createRectangleObjects(Arena *arena, size_t count) { + RenderObjects_Rect result = {0}; + result.count = count; + + result.p0.buf = PushFullList(arena, RLVec2List, count); + result.p1.buf = PushFullList(arena, RLVec2List, count); + result.color.buf = PushFullList(arena, RLVec4List, count); + result.borderRadius.buf = PushFullList(arena, FloatList, count); + result.borderThickness.buf = PushFullList(arena, FloatList, count); + result.edgeSoftness.buf = PushFullList(arena, FloatList, count); + + glGenVertexArrays(1, &result.vao); + + glGenBuffers(1, &result.p0.bufId); + glGenBuffers(1, &result.p1.bufId); + glGenBuffers(1, &result.color.bufId); + glGenBuffers(1, &result.borderRadius.bufId); + glGenBuffers(1, &result.borderThickness.bufId); + glGenBuffers(1, &result.edgeSoftness.bufId); + glGenBuffers(1, &result.edgeSoftness.bufId); + + glBindVertexArray(result.vao); + + int32 bufItemSize = sizeof(result.p0.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.p0.bufId); + glBufferData(GL_ARRAY_BUFFER, result.p0.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, bufItemSize, NULL); + glVertexAttribDivisor(0, 1); + glEnableVertexAttribArray(0); + + bufItemSize = sizeof(result.p1.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.p1.bufId); + glBufferData(GL_ARRAY_BUFFER, result.p1.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, bufItemSize, NULL); + glVertexAttribDivisor(1, 1); + glEnableVertexAttribArray(1); + + bufItemSize = sizeof(result.color.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.color.bufId); + glBufferData(GL_ARRAY_BUFFER, result.color.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, bufItemSize, NULL); + glVertexAttribDivisor(2, 1); + glEnableVertexAttribArray(2); + + bufItemSize = sizeof(result.borderRadius.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.borderRadius.bufId); + glBufferData(GL_ARRAY_BUFFER, result.borderRadius.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, bufItemSize, NULL); + glVertexAttribDivisor(3, 1); + glEnableVertexAttribArray(3); + + bufItemSize = sizeof(result.borderThickness.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.borderThickness.bufId); + glBufferData(GL_ARRAY_BUFFER, result.borderThickness.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribPointer(4, 1, GL_FLOAT, GL_FALSE, bufItemSize, NULL); glVertexAttribDivisor(4, 1); glEnableVertexAttribArray(4); - glBindBuffer(GL_ARRAY_BUFFER, result.edgeSoftnessBufferId); - glBufferData(GL_ARRAY_BUFFER, result.edgeSoftness.length * sizeof(FloatList_underlying), 0, GL_DYNAMIC_DRAW); - glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, sizeof(FloatList_underlying), NULL); + bufItemSize = sizeof(result.edgeSoftness.buf.data[0]); + glBindBuffer(GL_ARRAY_BUFFER, result.edgeSoftness.bufId); + glBufferData(GL_ARRAY_BUFFER, result.edgeSoftness.buf.length * bufItemSize, 0, GL_STREAM_DRAW); + glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, bufItemSize, NULL); glVertexAttribDivisor(5, 1); glEnableVertexAttribArray(5); return result; } -void updateRectangleObjectBuffers(Renderer *r) { +#define GL_UpdateBuffer(buffer) \ + glBindBuffer(GL_ARRAY_BUFFER, (buffer).bufId);\ + glBufferSubData(GL_ARRAY_BUFFER, 0, (buffer).buf.length * sizeof((buffer).underlying), (buffer).buf.data); + +static void updateCharObjectBuffers(Renderer *r) { + glBindVertexArray(r->chars.vao); + GL_UpdateBuffer(r->chars.begin); + GL_UpdateBuffer(r->chars.glyph); + GL_UpdateBuffer(r->chars.lineHeight); + GL_UpdateBuffer(r->chars.color); +} + + +static void updateRectangleObjectBuffers(Renderer *r) { glBindVertexArray(r->rects.vao); - - glBindBuffer(GL_ARRAY_BUFFER, r->rects.p0BufferId); - glBufferSubData(GL_ARRAY_BUFFER, 0, r->rects.p0.length * sizeof(RLVec2List_underlying), r->rects.p0.data); - - glBindBuffer(GL_ARRAY_BUFFER, r->rects.p1BufferId); - glBufferSubData(GL_ARRAY_BUFFER, 0, r->rects.p1.length * sizeof(RLVec2List_underlying), r->rects.p1.data); - - glBindBuffer(GL_ARRAY_BUFFER, r->rects.colorBufferId); - glBufferSubData(GL_ARRAY_BUFFER, 0, r->rects.color.length * sizeof(RLVec4List_underlying), r->rects.color.data); - - glBindBuffer(GL_ARRAY_BUFFER, r->rects.borderRadiusBufferId); - glBufferSubData(GL_ARRAY_BUFFER, 0, r->rects.borderRadius.length * sizeof(FloatList_underlying), r->rects.borderRadius.data); - - glBindBuffer(GL_ARRAY_BUFFER, r->rects.borderThicknessBufferId); - glBufferSubData(GL_ARRAY_BUFFER, 0, r->rects.borderThickness.length * sizeof(FloatList_underlying), r->rects.borderThickness.data); - - glBindBuffer(GL_ARRAY_BUFFER, r->rects.edgeSoftnessBufferId); - glBufferSubData(GL_ARRAY_BUFFER, 0, r->rects.edgeSoftness.length * sizeof(FloatList_underlying), r->rects.edgeSoftness.data); + GL_UpdateBuffer(r->rects.p0); + GL_UpdateBuffer(r->rects.p1); + GL_UpdateBuffer(r->rects.color); + GL_UpdateBuffer(r->rects.borderRadius); + GL_UpdateBuffer(r->rects.borderThickness); + GL_UpdateBuffer(r->rects.edgeSoftness); } Renderer createRenderer(Arena *arena, Scene *scene, Camera *cam, int32 light) { @@ -90,16 +147,23 @@ Renderer createRenderer(Arena *arena, Scene *scene, Camera *cam, int32 light) { .light = light, .camera = cam, .rects = createRectangleObjects(arena, 1024), + .chars = createCharObjects(arena, 10000), }; } void renderBegin(Renderer *r) { - r->rects.p0.length = 0; - r->rects.p1.length = 0; - r->rects.color.length = 0; - r->rects.borderRadius.length = 0; - r->rects.borderThickness.length = 0; - r->rects.edgeSoftness.length = 0; + r->rects.p0.buf.length = 0; + r->rects.p1.buf.length = 0; + r->rects.color.buf.length = 0; + r->rects.borderRadius.buf.length = 0; + r->rects.borderThickness.buf.length = 0; + r->rects.edgeSoftness.buf.length = 0; + + r->chars.begin.buf.length = 0; + r->chars.glyph.buf.length = 0; + r->chars.lineHeight.buf.length = 0; + r->chars.color.buf.length = 0; + r->sceneWidth = 0; r->sceneHeight = 0; r->sceneX = 0; @@ -107,10 +171,11 @@ void renderBegin(Renderer *r) { } void renderEnd(Renderer *r) { - // 3D Entities + // 3D Scene glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); glEnable(GL_DEPTH_TEST); + glViewport(r->sceneX, r->height - r->sceneY - r->sceneHeight, r->sceneWidth, r->sceneHeight); cameraSetAspect(r->camera, r->sceneWidth, r->sceneHeight); @@ -124,8 +189,6 @@ void renderEnd(Renderer *r) { glBindVertexArray(r->cubeMesh->vao); - // TODO(djledda): sort by mesh, texture, etc. - int model_uniform = getUniformLocation(r->phongShader, "model"); int solid_color_uniform = getUniformLocation(r->phongShader, "solid_color"); for (EachIn(r->scene->entities, i)) { @@ -141,29 +204,55 @@ void renderEnd(Renderer *r) { // 2D overlay glViewport(0, 0, r->width, r->height); - glUseProgram(r->solidShader->progId); - updateRectangleObjectBuffers(r); - glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); Matrix ortho = MatrixOrtho(0.0, r->width, r->height, 0.0, -1.0, 1.0); - setUniformMat4fv(r->solidShader, "projection", &ortho); + // 1. Rects + updateRectangleObjectBuffers(r); + setUniformMat4fv(r->solidShader, "projection", &ortho); glBindVertexArray(r->rects.vao); - glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, r->rects.p0.length); + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, r->rects.p0.buf.length); + + // 2. Text + updateCharObjectBuffers(r); + glUseProgram(r->textShader->progId); + setUniformMat4fv(r->textShader, "projection", &ortho); + glEnable(GL_TEXTURE_2D); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, r->activeFont->texId); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_BUFFER, r->activeFont->glyphTableTexId); + setUniform1i(r->textShader, "glyph_table", 1); + + glBindVertexArray(r->chars.vao); + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, r->chars.begin.buf.length); + glDisable(GL_BLEND); } void rendererPlaceRectangle(Renderer *r, real32 x, real32 y, real32 width, real32 height, RLVector4 color, real32 borderRadius, real32 borderThickness) { - ListAppend(r->rects.p0, ((RLVector2){ x, y })); - ListAppend(r->rects.p1, ((RLVector2){ x + width, y + height })); - ListAppend(r->rects.color, color); - ListAppend(r->rects.borderRadius, borderRadius); - ListAppend(r->rects.borderThickness, borderThickness); - ListAppend(r->rects.edgeSoftness, 0.15f); + ListAppend(r->rects.p0.buf, ((RLVector2){ x, y })); + ListAppend(r->rects.p1.buf, ((RLVector2){ x + width, y + height })); + ListAppend(r->rects.color.buf, color); + ListAppend(r->rects.borderRadius.buf, borderRadius); + ListAppend(r->rects.borderThickness.buf, borderThickness); + ListAppend(r->rects.edgeSoftness.buf, 0.15f); } +void rendererPlaceString(Renderer *r, string s, real32 x, real32 y, RLVector4 color, real32 lineHeight) { + real32 ratio = lineHeight / r->activeFont->lineHeight; + real32 charWidth = ratio * r->activeFont->charWidth; + for (int i = 0; i < s.length; i++) { + ListAppend(r->chars.begin.buf, ((RLVector2){ .x=x + i*charWidth, .y=y })); + ListAppend(r->chars.glyph.buf, s.str[i] - 32); + ListAppend(r->chars.lineHeight.buf, ratio); + ListAppend(r->chars.color.buf, (color)); + } +} diff --git a/src/render.h b/src/render.h index 955ab3e..572a66d 100644 --- a/src/render.h +++ b/src/render.h @@ -4,42 +4,74 @@ #include "gfx/Shader.h" #include "gfx/gfx.h" #include "world/world.h" -#include "lib/djstdlib/core.h" +#include "common.h" -DefineList(RLVector2, RLVec2); -DefineList(RLVector4, RLVec4); -DefineList(real32, Float); +typedef struct RenderObject_BufferInt32 RenderObject_BufferInt32; +struct RenderObject_BufferInt32 { + IntList buf; + uint32 bufId; + IntList_underlying underlying; +}; +typedef struct RenderObject_BufferFloat RenderObject_BufferFloat; +struct RenderObject_BufferFloat { + FloatList buf; + uint32 bufId; + FloatList_underlying underlying; +}; +typedef struct RenderObject_BufferVec2 RenderObject_BufferVec2; +struct RenderObject_BufferVec2 { + RLVec2List buf; + uint32 bufId; + RLVec2List_underlying underlying; +}; +typedef struct RenderObject_BufferVec4 RenderObject_BufferVec4; +struct RenderObject_BufferVec4 { + RLVec4List buf; + uint32 bufId; + RLVec4List_underlying underlying; +}; -typedef struct RenderObjects_Rectangle RenderObjects_Rectangle; -struct RenderObjects_Rectangle { + +typedef struct RenderObjects_Rect RenderObjects_Rect; +struct RenderObjects_Rect { uint32 vao; uint64 count; + RenderObject_BufferVec2 p0; + RenderObject_BufferVec2 p1; + RenderObject_BufferVec4 color; + RenderObject_BufferFloat borderRadius; + RenderObject_BufferFloat borderThickness; + RenderObject_BufferFloat edgeSoftness; +}; - RLVec2List p0; - uint32 p0BufferId; +typedef struct RenderObjects_Char RenderObjects_Char; +struct RenderObjects_Char { + uint32 vao; + uint64 count; + RenderObject_BufferVec2 begin; + RenderObject_BufferInt32 glyph; + RenderObject_BufferFloat lineHeight; + RenderObject_BufferVec4 color; +}; - RLVec2List p1; - uint32 p1BufferId; - - RLVec4List color; - uint32 colorBufferId; - - FloatList borderRadius; - uint32 borderRadiusBufferId; - - FloatList borderThickness; - uint32 borderThicknessBufferId; - - FloatList edgeSoftness; - uint32 edgeSoftnessBufferId; +typedef struct GlyphData GlyphData; +struct GlyphData { + real32 x0; + real32 y0; + real32 x1; + real32 y1; }; typedef struct Renderer Renderer; struct Renderer { - RenderObjects_Rectangle rects; + RenderObjects_Rect rects; + RenderObjects_Char chars; Shader *phongShader; Shader *solidShader; + Shader *textShader; + + Font *activeFont; int32 width; int32 height; @@ -60,7 +92,7 @@ Renderer createRenderer(Arena *arena, Scene *scene, Camera *cam, int32 light); void renderBegin(Renderer *r); void renderEnd(Renderer *r); void rendererPlaceRectangle(Renderer *r, real32 x, real32 y, real32 width, real32 height, RLVector4 color, real32 borderRadius, real32 borderThickness); -void updateRectangleObjectBuffers(Renderer *r); +void rendererPlaceString(Renderer *r, string s, real32 x, real32 y, RLVector4 color, real32 fontSize); #define Render(r) DeferLoop(renderBegin((r)), renderEnd((r))) #endif diff --git a/src/ui.c b/src/ui.c index 950d815..f948b37 100644 --- a/src/ui.c +++ b/src/ui.c @@ -2,6 +2,9 @@ #include "GLFW/glfw3.h" #include "lib/djstdlib/core.h" #include +#include + +UI_Context *__UI_current_ctx__ = NULL; static bool glfwMouse(GLFWwindow *window, int mouseBtnCode) { switch (glfwGetMouseButton(window, mouseBtnCode)) { @@ -48,6 +51,7 @@ UI_Context ui_initContext(Arena *arena, Renderer *renderer) { UI_RectList list = PushListZero(arena, UI_RectList, Thousand(10)); ListAppend(list, ((UI_Rect){0})); // empty item return (UI_Context){ + .arena=arena, .rects=list, .prevRects=prevList, .hotNode = 0, @@ -58,30 +62,16 @@ UI_Context ui_initContext(Arena *arena, Renderer *renderer) { }; } -static void ui_playground(UI_Context *ui) { - UI(ui, - .width=ui->renderer->width, - .height=ui->renderer->height, - .color=COLOR_WHITE, - ) { - UI(ui, - .color=COLOR_GREEN, - .childGap=10, - .flags=UI_Flag_HeightGrow, - .padding=UI_PadUniform(10), - ) { - for (int i = 0; i < 4; i++) { - UI(ui, - .width=100, - .flags=UI_Flag_HeightGrow, - .color=COLOR_RED, - .borderRadius=10, - ); - } - } - - UI(ui, .color=COLOR_CYAN, .flags=UI_Flag_HeightGrow | UI_Flag_WidthGrow); +void ui_placeText(UI_Context *ui, UI_RectStr strData) { + UI_RectStr *strDataCopy = PushStruct(ui->arena, UI_RectStr); + *strDataCopy = strData; + if (strDataCopy->lineHeight == -1) { + strDataCopy->lineHeight = ui->renderer->activeFont->lineHeight; } + strDataCopy->s = PushString(ui->arena, strData.s.length); + memcpy(strDataCopy->s.str, strData.s.str, strData.s.length); + UI_Rect *currRect = &ui->rects.data[ui->currRect]; + currRect->stringData = strDataCopy; } void ui_sizingPass(UI_Context *ui, bool isXAxis, int32 rectHandle) { @@ -116,18 +106,22 @@ void ui_sizingPass(UI_Context *ui, bool isXAxis, int32 rectHandle) { while (childHandle) { child = &ui->rects.data[childHandle]; - if (isVertical && (child->flags & UI_Flag_WidthGrow) && isXAxis) { - child->resolvedWidth = rect->resolvedWidth - rect->padding.left - rect->padding.right; - } - if (!isVertical && (child->flags & UI_Flag_HeightGrow) && !isXAxis) { - child->resolvedHeight = rect->resolvedHeight - rect->padding.top - rect->padding.bottom; - } - - if (child->flags & UI_Flag_WidthGrow && !isVertical && isXAxis) { - child->resolvedWidth += childBreadthInc; - } - if (child->flags & UI_Flag_HeightGrow && isVertical && !isXAxis) { - child->resolvedHeight += childBreadthInc; + if (isXAxis) { + if (child->flags & UI_Flag_WidthGrow) { + if (isVertical) { + child->resolvedWidth = rect->resolvedWidth - rect->padding.left - rect->padding.right; + } else { + child->resolvedWidth += childBreadthInc; + } + } + } else { + if (child->flags & UI_Flag_HeightGrow) { + if (!isVertical) { + child->resolvedHeight = rect->resolvedHeight - rect->padding.top - rect->padding.bottom; + } else { + child->resolvedHeight += childBreadthInc; + } + } } ui_sizingPass(ui, isXAxis, childHandle); @@ -142,7 +136,9 @@ void ui_calcLayout(UI_Context *ui, bool isXAxis, int32 rectHandle) { bool isVertical = (rect->flags & UI_Flag_Vertical); - real32 coord = isXAxis ? rect->x + rect->xOffset + rect->padding.left : rect->y + rect->yOffset + rect->padding.top; + real32 coord = isXAxis + ? rect->x + rect->xOffset + rect->padding.left + : rect->y + rect->yOffset + rect->padding.top; int32 childHandle = rect->firstChild; UI_Rect *child; @@ -150,9 +146,13 @@ void ui_calcLayout(UI_Context *ui, bool isXAxis, int32 rectHandle) { child = &ui->rects.data[childHandle]; if (isXAxis) { - child->x = coord; + child->x = child->flags & UI_Flag_Center && isVertical + ? coord + (rect->resolvedWidth - rect->padding.left - rect->padding.right)/2 - child->resolvedWidth/2 + : coord; } else { - child->y = coord; + child->y = child->flags & UI_Flag_Center && !isVertical + ? coord + (rect->resolvedHeight - rect->padding.top - rect->padding.bottom)/2 - child->resolvedHeight/2 + : coord; } ui_calcLayout(ui, isXAxis, childHandle); @@ -167,6 +167,8 @@ void ui_calcLayout(UI_Context *ui, bool isXAxis, int32 rectHandle) { } void ui_begin(UI_Context *ui) { + __UI_current_ctx__ = ui; + arenaFreeFrom(ui->arena, 0); ClearList(ui->prevRects); ListAppendList(ui->prevRects, ui->rects); ui->cursorIsPointer = false; @@ -204,6 +206,17 @@ void ui_end(UI_Context *ui) { rect->borderRadius, rect->borderThickness ); + if (rect->stringData) { + UI_RectStr *str = rect->stringData; + rendererPlaceString( + ui->renderer, + str->s, + rect->x + str->xOffset, + rect->y + str->yOffset, + str->color, + str->lineHeight + ); + } } if (ui->scene3DHandle) { @@ -213,10 +226,12 @@ void ui_end(UI_Context *ui) { ui->renderer->sceneWidth = (int32)scene3DRect->resolvedWidth; ui->renderer->sceneHeight = (int32)scene3DRect->resolvedHeight; } + + __UI_current_ctx__ = NULL; } static bool pointInRect(real32 x, real32 y, UI_Rect rect) { - return x > rect.x && y > rect.y && x < (rect.x + rect.width) && y < (rect.y + rect.height); + return x > rect.x && y > rect.y && x < (rect.x + rect.resolvedWidth) && y < (rect.y + rect.resolvedHeight); } void ui_openElement(UI_Context *ui, UI_Rect rect) { @@ -252,18 +267,35 @@ void ui_closeElement(UI_Context *ui) { if (currRect->width != -1) { currRect->resolvedWidth = currRect->width; } - currRect->resolvedWidth += currRect->padding.left + currRect->padding.right; + if (currRect->minWidth == -1) { + if (currRect->stringData) { + real32 charWidth = currRect->stringData->lineHeight / ui->renderer->activeFont->lineHeight * ui->renderer->activeFont->charWidth; + currRect->minWidth = charWidth*currRect->stringData->s.length; + } else { + currRect->minWidth = 0; + } + } + currRect->resolvedWidth = currRect->padding.left + currRect->padding.right + (currRect->resolvedWidth < currRect->minWidth ? currRect->minWidth : currRect->resolvedWidth); + if (currRect->height != -1) { currRect->resolvedHeight = currRect->height; + } else if (currRect->minHeight == -1) { + if (currRect->stringData) { + currRect->minHeight = currRect->stringData->lineHeight; + } else { + currRect->minHeight = 0; + } } - currRect->resolvedHeight += currRect->padding.top + currRect->padding.bottom; + currRect->resolvedHeight = currRect->padding.top + currRect->padding.bottom + (currRect->resolvedHeight < currRect->minHeight ? currRect->minHeight : currRect->resolvedHeight); bool vertical = parentRect->flags & UI_Flag_Vertical; real32 currBreadth = vertical ? currRect->resolvedHeight : currRect->resolvedWidth; real32 currCrossBreadth = vertical ? currRect->resolvedWidth : currRect->resolvedHeight; real32 parentBreadth = vertical ? parentRect->height : parentRect->width; + real32 currParentBreadth = vertical ? parentRect->resolvedHeight : parentRect->resolvedWidth; real32 parentCrossBreadth = vertical ? parentRect->width : parentRect->height; + real32 currParentCrossBreadth = vertical ? parentRect->resolvedWidth : parentRect->resolvedHeight; real32 gap = parentRect->childGap; real32 breadthInc = (parentRect->firstChild == ui->currRect ? 0 : gap) + currBreadth; @@ -276,7 +308,7 @@ void ui_closeElement(UI_Context *ui) { } } - real32 newCrossBreadth = currCrossBreadth > parentCrossBreadth ? currCrossBreadth : parentCrossBreadth; + real32 newCrossBreadth = currCrossBreadth > currParentCrossBreadth ? currCrossBreadth : currParentCrossBreadth; if (parentCrossBreadth == -1) { if (vertical) { parentRect->resolvedWidth = newCrossBreadth; @@ -288,8 +320,8 @@ void ui_closeElement(UI_Context *ui) { ui->currRect = ui->rects.data[ui->currRect].parent; } -bool ui_Button(UI_Context *ui, UI_Rect rect) { - int32 id = UI_NextID(ui); +bool ui_Button(UI_Context *ui, UI_Rect rect, UI_RectStr textAttr) { + int32 id = UI_NextID(); bool clicked = false; if (pointInRect(ui->input->mouse.point.x, ui->input->mouse.point.y, ui->prevRects.data[id])) { @@ -302,8 +334,9 @@ bool ui_Button(UI_Context *ui, UI_Rect rect) { } rect.borderRadius = 5; - rect.borderThickness = 0; - UI_FromRect(ui, rect); + UI_FromRect(rect) { + ui_placeText(ui, textAttr); + } return clicked; } @@ -312,7 +345,7 @@ bool ui_Button(UI_Context *ui, UI_Rect rect) { * Returns whether the checkbox was clicked */ bool ui_CheckboxRect(UI_Context *ui, bool *value, UI_Rect rect) { - int32 id = UI_NextID(ui); + int32 id = UI_NextID(); bool clicked = false; if (pointInRect(ui->input->mouse.point.x, ui->input->mouse.point.y, ui->prevRects.data[id])) { ui->cursorIsPointer = true; @@ -326,12 +359,12 @@ bool ui_CheckboxRect(UI_Context *ui, bool *value, UI_Rect rect) { if (*value) { rect.borderRadius = 5; rect.borderThickness = 0; - UI_FromRect(ui, rect); + UI_FromRect(rect); } else { rect.borderRadius = 5; rect.borderThickness = 2; rect.color = COLOR_WHITE; - UI_FromRect(ui, rect); + UI_FromRect(rect); } return clicked; diff --git a/src/ui.h b/src/ui.h index 3032b9b..f836161 100644 --- a/src/ui.h +++ b/src/ui.h @@ -41,6 +41,7 @@ enum UI_Flag { UI_Flag_HeightGrow=1<<1, UI_Flag_Vertical=1<<2, // Default is horizontal UI_Flag_3DScene=1<<3, + UI_Flag_Center=1<<4, // .. UI_Flag_COUNT, }; @@ -53,9 +54,18 @@ struct UI_Padding { real32 left; }; +typedef struct UI_RectStr UI_RectStr; +struct UI_RectStr { + string s; + real32 xOffset; + real32 yOffset; + real32 lineHeight; + RLVector4 color; +}; + typedef struct UI_Rect UI_Rect; struct UI_Rect { - // UI_Rect_LayoutFlag + /** UI_Rect_LayoutFlag */ uint64 flags; int32 parent; @@ -87,11 +97,14 @@ struct UI_Rect { real32 y; real32 resolvedWidth; real32 resolvedHeight; + + UI_RectStr *stringData; }; DefineList(UI_Rect, UI_Rect); typedef struct UI_Context UI_Context; struct UI_Context { + Arena *arena; UI_RectList prevRects; UI_RectList rects; int32 hotNode; @@ -114,16 +127,21 @@ void ui_end(UI_Context *ui); UI_Context ui_initContext(Arena *arena, Renderer *renderer); void ui_resolve(); void ui_rect(UI_Context *ui, UI_Rect rect); +void ui_placeText(UI_Context *ui, UI_RectStr strData); void ui_openElement(UI_Context *ui, UI_Rect rect); void ui_closeElement(UI_Context *ui); bool ui_CheckboxRect(UI_Context *ui, bool *value, UI_Rect rect); -#define UI(ui, ...) DeferLoop(ui_openElement((ui), UI_CreateRect(__VA_ARGS__)), ui_closeElement((ui))) -#define UI_FromRect(ui, rect) DeferLoop(ui_openElement((ui), (rect)), ui_closeElement((ui))) -#define UI_CreateRect(...) ((UI_Rect){.width=-1, .height=-1, __VA_ARGS__}) +extern UI_Context *__UI_current_ctx__; + +#define UI(...) DeferLoop(ui_openElement((__UI_current_ctx__), UI_CreateRect(__VA_ARGS__)), ui_closeElement((__UI_current_ctx__))) +#define UI_CreateRect(...) ((UI_Rect){.minHeight=-1, .minWidth=-1, .width=-1, .height=-1, __VA_ARGS__}) +#define UI_FromRect(rect) DeferLoop(ui_openElement((__UI_current_ctx__), (rect)), ui_closeElement((__UI_current_ctx__))) #define UI_Pass(ui) DeferLoop(ui_begin((ui)), ui_end((ui))) #define UI_PadUniform(padding) ((UI_Padding){ (padding), (padding), (padding), (padding) }) -#define UI_NextID(ui) ((ui)->rects.length) +#define UI_NextID() ((__UI_current_ctx__)->rects.length) +#define UI_Text(str, ...) (ui_placeText((__UI_current_ctx__), ((UI_RectStr){ .xOffset=0, .yOffset=0, .lineHeight=-1, .s=(str), __VA_ARGS__ }))) +#define UI_CreateText(str, ...) ((UI_RectStr){ .xOffset=0, .yOffset=0, .lineHeight=-1, .s=(str), __VA_ARGS__ }) #endif