diff --git a/examples/GLES3-GLFW-video-demo/.gitignore b/examples/GLES3-GLFW-video-demo/.gitignore new file mode 100644 index 0000000..d4fa566 --- /dev/null +++ b/examples/GLES3-GLFW-video-demo/.gitignore @@ -0,0 +1,2 @@ +/build/ +/website-demo-macos-glfw* diff --git a/examples/GLES3-GLFW-video-demo/CMakeLists.txt b/examples/GLES3-GLFW-video-demo/CMakeLists.txt new file mode 100644 index 0000000..01ae557 --- /dev/null +++ b/examples/GLES3-GLFW-video-demo/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.27) +project(GLES3_GLFW_video_demo C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# ------------------------------------------------- +# FetchContent +# ------------------------------------------------- +include(FetchContent) +set(FETCHCONTENT_QUIET FALSE) + +# ------------------------------------------------- +# STB (header-only) +# ------------------------------------------------- +FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG master +) +FetchContent_MakeAvailable(stb) + +# ------------------------------------------------- +# GLFW +# ------------------------------------------------- +FetchContent_Declare( + glfw + GIT_REPOSITORY https://github.com/glfw/glfw.git + GIT_TAG 3.4 + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(glfw) + +# Disable examples/tests/docs (important) +set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE) + +# ------------------------------------------------- +# Executable +# ------------------------------------------------- +add_executable(GLES3_GLFW_video_demo + main.c +) + +target_include_directories(GLES3_GLFW_video_demo + PUBLIC + . + ../.. + ${stb_SOURCE_DIR} +) + +# ------------------------------------------------- +# Link libraries +# ------------------------------------------------- +target_link_libraries(GLES3_GLFW_video_demo + PRIVATE + glfw +) + +# ------------------------------------------------- +# Platform-specific OpenGL / GLES +# ------------------------------------------------- +if(APPLE) + find_library(OPENGL_FRAMEWORK OpenGL) + target_link_libraries(GLES3_GLFW_video_demo PRIVATE ${OPENGL_FRAMEWORK}) + + # Needed for GLFW on macOS + target_link_libraries(GLES3_GLFW_video_demo + PRIVATE + "-framework Cocoa" + "-framework IOKit" + "-framework CoreVideo" + ) +elseif(WIN32) + target_link_libraries(GLES3_GLFW_video_demo PRIVATE opengl32) +elseif(UNIX) + target_link_libraries(GLES3_GLFW_video_demo PRIVATE GL) +endif() + +# ------------------------------------------------- +# Build flags (kept minimal) +# ------------------------------------------------- +if(MSVC) + target_compile_options(GLES3_GLFW_video_demo PRIVATE /W3) +else() + target_compile_options(GLES3_GLFW_video_demo PRIVATE -Wall -Wextra) +endif() + +# ------------------------------------------------- +# Copy resources +# ------------------------------------------------- +add_custom_command( + TARGET GLES3_GLFW_video_demo POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/resources + ${CMAKE_CURRENT_BINARY_DIR}/resources +) diff --git a/examples/GLES3-GLFW-video-demo/Makefile.emscripten b/examples/GLES3-GLFW-video-demo/Makefile.emscripten new file mode 100644 index 0000000..e0ea72b --- /dev/null +++ b/examples/GLES3-GLFW-video-demo/Makefile.emscripten @@ -0,0 +1,33 @@ +# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab: + +# +# PROGRAM COMPONENTS +# + +CXX = emcc + +CXXFLAGS = -std=c99 +CXXFLAGS += -O0 +CXXFLAGS += -I../.. +CXXFLAGS += -I./build/_deps/stb-src + +LDLIBS += -s USE_ZLIB=1 +LDLIBS += -s USE_GLFW=3 +LDLIBS += -s FULL_ES2=1 -s USE_WEBGL2=1 +LDLIBS += -s ALLOW_MEMORY_GROWTH=1 -s GL_UNSAFE_OPTS=0 +LDLIBS += -s STACK_SIZE=2048kb +LDLIBS += -s EXPORTED_FUNCTIONS=['_main'] +LDLIBS += -s ASSERTIONS=1 -s SAFE_HEAP=1 +LDLIBS += --preload-file $(PWD)/resources/Roboto-Regular.ttf@resources/Roboto-Regular.ttf + +main: + mkdir -p build/emscripten + time $(CXX) $(CXXFLAGS) \ + $(PWD)/main.c \ + $(LDLIBS) -o build/emscripten/index.html + +test: + make -f Makefile.emscripten main \ + && (cd build/emscripten && python3 -mhttp.server) + +.PHONY: main diff --git a/examples/GLES3-GLFW-video-demo/Makefile.macos b/examples/GLES3-GLFW-video-demo/Makefile.macos new file mode 100644 index 0000000..0ad42f3 --- /dev/null +++ b/examples/GLES3-GLFW-video-demo/Makefile.macos @@ -0,0 +1,29 @@ +# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab: + +CXX = clang + +CXXFLAGS = -std=c99 +CXXFLAGS += -g -O0 -fno-omit-frame-pointer +CXXFLAGS += -ferror-limit=1 +CXXFLAGS += -I../.. +CXXFLAGS += -I./build/_deps/stb-src +CXXFLAGS += -DGL_SILENCE_DEPRECATION + +# GLFW (Homebrew) +CXXFLAGS += -I$(shell brew --prefix glfw)/include +LDLIBS += -L$(shell brew --prefix glfw)/lib -lglfw + + +# macOS system frameworks (OpenGL needs these) +LDLIBS += -framework OpenGL +LDLIBS += -framework Cocoa +LDLIBS += -framework IOKit +LDLIBS += -framework CoreVideo + +main: + mkdir -p build + + time $(CXX) $(CXXFLAGS) \ + $(PWD)/main.c \ + $(LDLIBS) \ + -o build/website-demo-macos-glfw \ No newline at end of file diff --git a/examples/GLES3-GLFW-video-demo/README.md b/examples/GLES3-GLFW-video-demo/README.md new file mode 100644 index 0000000..c11cf9d --- /dev/null +++ b/examples/GLES3-GLFW-video-demo/README.md @@ -0,0 +1,55 @@ +GLES3 Renderer Video Demo (Using GLFW) +====================================== + +This directory contains a standard Video-Demo example +using work-in-progress GLES3 renderer. +While it still needs refinement, the renderer is already functional and demonstrates the core rendering pipeline. + +Current features + +- Supports all draw commands except custom. +- In the best-case scenario (no clipping): +- All quad-based commands (Rectangle, Image, Border) are rendered in a single draw call. +- All glyphs belonging to the same font are rendered in one instanced draw call. +- When clipping (scissoring) is used: +- The renderer flushes draw calls before and after each scissor region. +- Supports up to 4 fonts and 4 image textures. +- Image textures may also be used as texture atlases. +- Custom UserData provides per-image UV coordinates, allowing multiple images to share a single OpenGL texture. +- Uses stb_image.h and stb_truetype.h as single-header dependencies for asset loading. +- The loading layer is modular and can be replaced with a different asset pipeline if needed. + +Currently builds on: +- Emscripten +- clang++ / macOS +- CMake support is not available yet. + +Windowing and platform support + +This example uses GLFW, and the renderer is framework agnostic + +How to build it with CMake? +--------------------------- + +Cmake build is the easiest way to build it: + + mkdir build + cmake -S . -B ./build + +How to build and run on Emscripten: +---------------------------------- + +For Emscripten the build is a bit custom, +but it depends on CMakeBuild to install header-stb dependency. +So you still need to build it with CMake first. + +And then you have to source the Emscripten SDK: + + source /path/to/emscripten/emsdk/emsdk_env.sh + +Then build it with hand-crafted Makefile.emscripten: + + make -f Makefile.emscripten test + +and then navigate to http://localhost:8080 + diff --git a/examples/GLES3-GLFW-video-demo/main.c b/examples/GLES3-GLFW-video-demo/main.c new file mode 100644 index 0000000..5b70a42 --- /dev/null +++ b/examples/GLES3-GLFW-video-demo/main.c @@ -0,0 +1,207 @@ +#include + +#include + +#define STB_IMAGE_IMPLEMENTATION +#define STB_TRUETYPE_IMPLEMENTATION +#define CLAY_IMPLEMENTATION +#define CLAY_RENDERER_GLES3_IMPLEMENTATION + +#include + +#include "../../renderers/GLES3/clay_renderer_gles3.h" +#include "../shared-layouts/clay-video-demo.c" +#include "../../renderers/GLES3/clay_renderer_gles3_loader_stb.c" + +typedef struct VideoCtx +{ + int shouldContinue; + GLFWwindow *glfwWindow; + int screenWidth, screenHeight; +} VideoCtx; + +VideoCtx g_ctx; + +static int initVideo(VideoCtx *ctx, const int initialWidth, const int initialHeight) +{ + if (!glfwInit()) + { + fprintf(stderr, "Failed to init GLFW\n"); + return 0; + } + + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + glfwWindowHint(GLFW_SAMPLES, 4); // enable multisampling + + // NO solution for high DPI yet + glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE); + + g_ctx.glfwWindow = glfwCreateWindow(initialWidth, initialHeight, "GLES3 GLFW Video Demo", NULL, NULL); + + if (g_ctx.glfwWindow == NULL) + { + fprintf(stderr, "Failed to create GLFW window\n"); + glfwTerminate(); + return 0; + } + glfwMakeContextCurrent(g_ctx.glfwWindow); + + glfwGetWindowSize(g_ctx.glfwWindow, &g_ctx.screenWidth, &g_ctx.screenHeight); + glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight); + printf("Frame buffer size %dx%d\n", g_ctx.screenWidth, g_ctx.screenHeight); + + + glEnable(GL_BLEND); + // Enables blending, which allows transparent textures to be rendered properly. + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // Sets the blending function. + // - `GL_SRC_ALPHA`: Uses the alpha value of the source (texture or color). + // - `GL_ONE_MINUS_SRC_ALPHA`: Makes the destination color blend with the background based on alpha. + // This is commonly used for standard transparency effects. + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable(GL_DEPTH_TEST); + // Enables depth testing, ensuring that objects closer to the camera are drawn in front of those farther away. + // This prevents objects from rendering incorrectly based on draw order. + + return 1; +} + +void My_ErrorHandler(Clay_ErrorData errorData) +{ + printf("[ClaY ErroR] %s", errorData.errorText.chars); +} + +Stb_FontData g_stbFonts[MAX_FONTS]; // Fonts userData +Gles3_Renderer g_gles3; // The renderer itself + +static Clay_Vector2 g_scrollDelta = {0.0f, 0.0f}; +static double g_lastTime = 0.0; +static double g_deltaTime = 0.0; +static void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) +{ + g_scrollDelta.x += (float)xoffset; + g_scrollDelta.y += (float)yoffset; +} + +void init() +{ + size_t clayRequiredMemory = Clay_MinMemorySize(); + g_gles3.clayMemory = (Clay_Arena){ + .capacity = clayRequiredMemory, + .memory = (char *)malloc(clayRequiredMemory), + }; + Clay_Context *clayCtx = Clay_Initialize( + g_gles3.clayMemory, + (Clay_Dimensions){ + .width = (float)g_ctx.screenWidth, + .height = (float)g_ctx.screenHeight, + }, + (Clay_ErrorHandler){ + .errorHandlerFunction = My_ErrorHandler, + }); + + // Note that MeasureText has to be set after the Context is set! + Clay_SetCurrentContext(clayCtx); + Clay_SetMeasureTextFunction(Stb_MeasureText, &g_stbFonts); + // This example uses stb loader, but you can inject your custom loader + // to load Images and Fonts if you don't want to use STB library + Gles3_SetRenderTextFunction(&g_gles3, Stb_RenderText, &g_stbFonts); + + Gles3_Initialize(&g_gles3, 4096); + + int atlasW = 1024; + int atlasH = 1024; + if (!Stb_LoadFont( + &g_gles3.fontTextures[0], + &g_stbFonts[0], + "resources/Roboto-Regular.ttf", + 24.0f, // bake pixel height + atlasW, + atlasH)) + abort(); + + Clay_SetDebugModeEnabled(true); + glfwSetScrollCallback(g_ctx.glfwWindow, scroll_callback); +} + + +void loop() +{ + Clay_Vector2 scrollDelta = {0.0f, 0.0f}; + + glfwPollEvents(); + + /* Quit handling */ + if (glfwWindowShouldClose(g_ctx.glfwWindow)) + { + g_ctx.shouldContinue = false; + } + + /* Consume scroll delta (accumulated via callback) */ + scrollDelta = g_scrollDelta; + g_scrollDelta.x = 0.0f; + g_scrollDelta.y = 0.0f; + + /* Delta time (milliseconds, like your SDL version) */ + double now = glfwGetTime(); // seconds + g_deltaTime = (now - g_lastTime) * 1000.0; + g_lastTime = now; + + double mouseX = 0.0; + double mouseY = 0.0; + glfwGetCursorPos(g_ctx.glfwWindow, &mouseX, &mouseY); + + Clay_Vector2 mousePosition = { + (float)mouseX, + (float)mouseY + }; + + int mousePressed = + glfwGetMouseButton(g_ctx.glfwWindow, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; + + Clay_SetPointerState(mousePosition, mousePressed); + + Clay_UpdateScrollContainers( + true, + (Clay_Vector2){scrollDelta.x, scrollDelta.y}, + g_deltaTime); + + glfwGetWindowSize(g_ctx.glfwWindow, &g_ctx.screenWidth, &g_ctx.screenHeight); + glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight); + + Clay_SetLayoutDimensions((Clay_Dimensions){(float)g_ctx.screenWidth, (float)g_ctx.screenHeight}); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); // Clay renderer is simple and never writes to depth buffer + glClearColor(0.1f, 0.2f, 0.1f, 1.0f); + + ClayVideoDemo_Data data = ClayVideoDemo_Initialize(); + Clay_RenderCommandArray cmds = ClayVideoDemo_CreateLayout(&data); + + Gles3_Render(&g_gles3, cmds, g_stbFonts); + + glfwSwapBuffers(g_ctx.glfwWindow); + +} + +// Just initializes and spins the animation loop +int main() +{ + initVideo(&g_ctx, 1280, 720); + init(); + + g_ctx.shouldContinue = true; +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(loop, 0, 1); +#else + while (g_ctx.shouldContinue) + { + loop(); + } +#endif +} \ No newline at end of file diff --git a/examples/GLES3-GLFW-video-demo/resources/Roboto-Regular.ttf b/examples/GLES3-GLFW-video-demo/resources/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/examples/GLES3-GLFW-video-demo/resources/Roboto-Regular.ttf differ diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/.gitignore b/examples/GLES3-SDL2-sidebar-scrolling-container/.gitignore new file mode 100644 index 0000000..ed49aff --- /dev/null +++ b/examples/GLES3-SDL2-sidebar-scrolling-container/.gitignore @@ -0,0 +1,3 @@ +/build/ +/website-demo-macos-sdl2* +/macos-sidebar-scrolling-container* diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/CMakeLists.txt b/examples/GLES3-SDL2-sidebar-scrolling-container/CMakeLists.txt new file mode 100644 index 0000000..d47bfd0 --- /dev/null +++ b/examples/GLES3-SDL2-sidebar-scrolling-container/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.27) +set(CMAKE_C_STANDARD 99) +project(GLES3_SDL2_sidebar_scrolling_container C) + +# ------------------------------------------------- +# FetchContent +# ------------------------------------------------- +include(FetchContent) +set(FETCHCONTENT_QUIET FALSE) + +# ------------------------------------------------- +# STB (header-only) +# ------------------------------------------------- +FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG master +) +FetchContent_MakeAvailable(stb) + +# ------------------------------------------------- +# SDL2 +# ------------------------------------------------- +FetchContent_Declare( + SDL2 + GIT_REPOSITORY "https://github.com/libsdl-org/SDL.git" + GIT_TAG "release-2.30.10" + GIT_PROGRESS TRUE + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(SDL2) + +# ------------------------------------------------- +# Executable +# ------------------------------------------------- +add_executable(GLES3_SDL2_sidebar_scrolling_container main.c) +target_compile_options(GLES3_SDL2_sidebar_scrolling_container PUBLIC) +target_include_directories(GLES3_SDL2_sidebar_scrolling_container + PUBLIC + . # This renderer + ../.. # Clay + ${stb_SOURCE_DIR} # STB header only depencency that does not have its own CMake build + +) + +# ------------------------------------------------- +# Link libraries +# ------------------------------------------------- +target_link_libraries(GLES3_SDL2_sidebar_scrolling_container PUBLIC + SDL2::SDL2main + SDL2::SDL2-static +) + +# ------------------------------------------------- +# Platform-specific OpenGL / GLES +# ------------------------------------------------- +find_package(SDL2 REQUIRED) +find_library(OPENGL_FRAMEWORK OpenGL) +target_link_libraries(GLES3_SDL2_sidebar_scrolling_container + PRIVATE + ${OPENGL_FRAMEWORK} +) + +# ------------------------------------------------- +# Build flags (kept minimal) +# ------------------------------------------------- +if(MSVC) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") +else() + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") + set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") +endif() + +# ------------------------------------------------- +# Copy resources +# ------------------------------------------------- +add_custom_command( + TARGET GLES3_SDL2_sidebar_scrolling_container POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/resources + ${CMAKE_CURRENT_BINARY_DIR}/resources +) diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/Makefile.emscripten b/examples/GLES3-SDL2-sidebar-scrolling-container/Makefile.emscripten new file mode 100644 index 0000000..03e4ba4 --- /dev/null +++ b/examples/GLES3-SDL2-sidebar-scrolling-container/Makefile.emscripten @@ -0,0 +1,36 @@ +# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab: + +# +# PROGRAM COMPONENTS +# + +CXX = emcc + +CXXFLAGS = -std=c99 +CXXFLAGS += -O0 +CXXFLAGS += -I../.. +CXXFLAGS += -I./build/_deps/stb-src + +LDLIBS += -s USE_ZLIB=1 +LDLIBS += -s USE_SDL=2 +LDLIBS += -s FULL_ES2=1 -s USE_WEBGL2=1 +LDLIBS += -s ALLOW_MEMORY_GROWTH=1 -s GL_UNSAFE_OPTS=0 +LDLIBS += -s STACK_SIZE=2048kb +LDLIBS += -s EXPORTED_FUNCTIONS=['_main'] +LDLIBS += -s ASSERTIONS=1 -s SAFE_HEAP=1 +LDLIBS += --preload-file $(PWD)/resources/profile-picture.png@resources/profile-picture.png +LDLIBS += --preload-file $(PWD)/resources/millbank.jpeg@resources/millbank.jpeg +LDLIBS += --preload-file $(PWD)/resources/Roboto-Regular.ttf@resources/Roboto-Regular.ttf +LDLIBS += --preload-file $(PWD)/resources/RobotoMono-Medium.ttf@resources/RobotoMono-Medium.ttf + +main: + mkdir -p build/emscripten + time $(CXX) $(CXXFLAGS) \ + $(PWD)/main.c \ + $(LDLIBS) -o build/emscripten/index.html + +test: + make -f Makefile.emscripten main \ + && (cd build/emscripten && python3 -mhttp.server) + +.PHONY: main diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/Makefile.macos b/examples/GLES3-SDL2-sidebar-scrolling-container/Makefile.macos new file mode 100644 index 0000000..a0c325a --- /dev/null +++ b/examples/GLES3-SDL2-sidebar-scrolling-container/Makefile.macos @@ -0,0 +1,28 @@ +# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab: + +CXX = clang + +CXXFLAGS = -std=c99 +CXXFLAGS += -g -O0 -fno-omit-frame-pointer +CXXFLAGS += -ferror-limit=1 +CXXFLAGS += -I../.. +CXXFLAGS += -I./build/_deps/stb-src +CXXFLAGS += -DGL_SILENCE_DEPRECATION + +# SDL2 (Homebrew) +CXXFLAGS += -I$(shell brew --prefix sdl2)/include/SDL2 +LDLIBS += -L$(shell brew --prefix sdl2)/lib -lSDL2 + +# macOS system frameworks (OpenGL needs these) +LDLIBS += -framework OpenGL +LDLIBS += -framework Cocoa +LDLIBS += -framework IOKit +LDLIBS += -framework CoreVideo + +main: + mkdir -p build + + time $(CXX) $(CXXFLAGS) \ + $(PWD)/main.c \ + $(LDLIBS) \ + -o build/macos-sidebar-scrolling-container-sdl2 diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/README.md b/examples/GLES3-SDL2-sidebar-scrolling-container/README.md new file mode 100644 index 0000000..5927581 --- /dev/null +++ b/examples/GLES3-SDL2-sidebar-scrolling-container/README.md @@ -0,0 +1,57 @@ +GLES3 Renderer Scrolling Container (Using SDL2) +=============================================== + +This directory contains a complete example thatn can me used to test all different draw commands +using work-in-progress GLES3 renderer. +While it still needs refinement, the renderer is already functional and demonstrates the core rendering pipeline. + +The images used as resources in this example, namely: + +- millbank.jpeg a window with a panoramic city view; +- and profile-picture.png showing objects aligned in a circle; + +are taken with my phone camera and are dedicated to the Public Domain. + +How to build it with CMake? +--------------------------- + +Cmake build is the easiest way to build it: + + mkdir build + cmake -S . -B ./build + +How to build and run on Emscripten: +---------------------------------- + +For Emscripten the build is a bit custom, +but it depends on CMakeBuild to install header-stb dependency. +So you still need to build it with CMake first. + +And then you have to source the Emscripten SDK: + + source /path/to/emscripten/emsdk/emsdk_env.sh + +Then build it with hand-crafted Makefile.emscripten: + + make -f Makefile.emscripten test + +and then navigate to http://localhost:8080 + +How to build and run on Mac: +---------------------------- + +Requires SDL2: + + brew install sdl2 + +Requires STB: + + cmake -B build + +Build it with: + + make -f Makefile.macos + +And run with: + + ./macos-sidebar-scrolling-container-sdl2 diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/main.c b/examples/GLES3-SDL2-sidebar-scrolling-container/main.c new file mode 100644 index 0000000..8e480b8 --- /dev/null +++ b/examples/GLES3-SDL2-sidebar-scrolling-container/main.c @@ -0,0 +1,565 @@ +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#define STB_TRUETYPE_IMPLEMENTATION +#define CLAY_IMPLEMENTATION +#define CLAY_RENDERER_GLES3_IMPLEMENTATION + +#include + +#include "../../renderers/GLES3/clay_renderer_gles3.h" +#include "../shared-layouts/clay-video-demo.c" +#include "../../renderers/GLES3/clay_renderer_gles3_loader_stb.c" + +typedef struct VideoCtx +{ + int shouldContinue; + SDL_Window *sdlWindow; + SDL_GLContext sdlContext; + int screenWidth, screenHeight; +} VideoCtx; + +VideoCtx g_ctx; + +static int initVideo(VideoCtx *ctx, const int initialWidth, const int initialHeight) +{ + SDL_Init(SDL_INIT_VIDEO); + +#if defined(__EMSCRIPTEN__) + // OpenGL ES 3 profile + SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1"); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); +#else + // Apple MacOs will use it own legacy desktop GL instead + // I know, I lied, I said this was an GLES3 + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); +#endif + + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); + + g_ctx.sdlWindow = SDL_CreateWindow( + "SDL2 GLES3", + SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, + initialWidth, + initialHeight, + SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN); + g_ctx.sdlContext = SDL_GL_CreateContext(g_ctx.sdlWindow); + + SDL_ShowWindow(g_ctx.sdlWindow); + SDL_Delay(1); + SDL_GL_GetDrawableSize(g_ctx.sdlWindow, &g_ctx.screenWidth, &g_ctx.screenHeight); + glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight); + + glEnable(GL_BLEND); + // Enables blending, which allows transparent textures to be rendered properly. + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // Sets the blending function. + // - `GL_SRC_ALPHA`: Uses the alpha value of the source (texture or color). + // - `GL_ONE_MINUS_SRC_ALPHA`: Makes the destination color blend with the background based on alpha. + // This is commonly used for standard transparency effects. + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable(GL_DEPTH_TEST); + // Enables depth testing, ensuring that objects closer to the camera are drawn in front of those farther away. + // This prevents objects from rendering incorrectly based on draw order. + + return 1; +} + +void My_ErrorHandler(Clay_ErrorData errorData) +{ + printf("[ClaY ErroR] %s", errorData.errorText.chars); +} + +Stb_FontData g_stbFonts[MAX_FONTS]; // Fonts userData +Gles3_Renderer g_gles3; // The renderer itself + +Uint64 NOW = 0; +Uint64 LAST = 0; +double deltaTime = 0; + +uint64_t g_drawCallsDuringLastFrame = 0; + +double g_timeAccumulator = 0.0; +double g_avgFrameMs = 0.0; +double g_fps = 0.0; +int g_frameCount = 0; + +char g_fpsText[128]; +size_t g_fpsTextLen = 0; + +static double g_wallTimeAccumulator = 0.0; + +char g_glInfoText[512]; +size_t g_glInfoTextLen; + +void init() +{ + size_t clayRequiredMemory = Clay_MinMemorySize(); + g_gles3.clayMemory = (Clay_Arena){ + .capacity = clayRequiredMemory, + .memory = (char *)malloc(clayRequiredMemory), + }; + Clay_Context *clayCtx = Clay_Initialize( + g_gles3.clayMemory, + (Clay_Dimensions){ + .width = (float)g_ctx.screenWidth, + .height = (float)g_ctx.screenHeight, + }, + (Clay_ErrorHandler){ + .errorHandlerFunction = My_ErrorHandler, + }); + + // Note that MeasureText has to be set after the Context is set! + Clay_SetCurrentContext(clayCtx); + Clay_SetMeasureTextFunction(Stb_MeasureText, &g_stbFonts); + Gles3_SetRenderTextFunction(&g_gles3, Stb_RenderText, &g_stbFonts); + + Gles3_Initialize(&g_gles3, 4096); + + int atlasW = 1024; + int atlasH = 1024; + if (!Stb_LoadFont( + &g_gles3.fontTextures[0], + &g_stbFonts[0], + "resources/Roboto-Regular.ttf", + 24.0f, // bake pixel height + atlasW, + atlasH)) + abort(); + if (!Stb_LoadFont( + &g_gles3.fontTextures[1], + &g_stbFonts[1], + "resources/RobotoMono-Medium.ttf", + 24.0f, // bake pixel height + atlasW, + atlasH)) + abort(); + + if (!Stb_LoadImage( + &g_gles3.imageTextures[0], + "resources/profile-picture.png")) + abort(); + + if (!Stb_LoadImage( + &g_gles3.imageTextures[1], + "resources/millbank.jpeg")) + abort(); + + Clay_SetDebugModeEnabled(true); + + const GLubyte *glVersion = glGetString(GL_VERSION); + const GLubyte *glslVersion = glGetString(GL_SHADING_LANGUAGE_VERSION); + const GLubyte *vendor = glGetString(GL_VENDOR); + const GLubyte *renderer = glGetString(GL_RENDERER); + + g_glInfoTextLen = (size_t)snprintf( + g_glInfoText, + sizeof(g_glInfoText), + "OpenGL Version : %s\n" + "GLSL Version : %s\n" + "Vendor : %s\n" + "Renderer : %s", + glVersion ? (const char *)glVersion : "unknown", + glslVersion ? (const char *)glslVersion : "unknown", + vendor ? (const char *)vendor : "unknown", + renderer ? (const char *)renderer : "unknown"); +} + +Gles3_ImageConfig g_profilePicture = (Gles3_ImageConfig){ + .textureToUse = 0, + .u0 = 0.0f, + .v0 = 0.0f, + .u1 = 1.0f, + .v1 = 1.0f, +}; + +Gles3_ImageConfig g_window1 = (Gles3_ImageConfig){ + .textureToUse = 1, + .u0 = 0.0f, + .v0 = 0.35f, + .u1 = 0.18f, + .v1 = 0.75f, +}; +Gles3_ImageConfig g_window2 = (Gles3_ImageConfig){ + .textureToUse = 1, + .u0 = 0.25f, + .v0 = 0.35f, + .u1 = 0.47f, + .v1 = 0.75f, +}; +Gles3_ImageConfig g_window3 = (Gles3_ImageConfig){ + .textureToUse = 1, + .u0 = 0.52f, + .v0 = 0.35f, + .u1 = 0.76f, + .v1 = 0.75f, +}; +Gles3_ImageConfig g_window4 = (Gles3_ImageConfig){ + .textureToUse = 1, + .u0 = 0.82f, + .v0 = 0.35f, + .u1 = 1.0f, + .v1 = 0.75f, +}; + +Clay_LayoutConfig dropdownTextItemLayout = {.padding = {8, 8, 4, 4}}; +Clay_TextElementConfig dropdownTextElementConfig = {.fontSize = 24, .textColor = {55, 55, 55, 255}}; +void RenderDropdownTextItem(int index) +{ + CLAY_AUTO_ID({.layout = dropdownTextItemLayout, .backgroundColor = {220, 220, 220, 255}}) + { + CLAY_TEXT(CLAY_STRING("I'm a text field in a scroll container."), &dropdownTextElementConfig); + } +} + +const uint32_t FONT_ID_BODY_24 = 1; + +Clay_String profileText = CLAY_STRING_CONST("Profile Page one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen"); + +void RenderHeaderButton1(Clay_String text) +{ + CLAY_AUTO_ID( + {.layout = { + .padding = {16, 16, 8, 8}}, + .backgroundColor = {140, 140, 140, 255}, + .border = {.width = CLAY_BORDER_OUTSIDE(14), .color = {180, 80, 80, 255}}, + .cornerRadius = CLAY_CORNER_RADIUS(5)}) + { + CLAY_TEXT(text, CLAY_TEXT_CONFIG({.fontId = FONT_ID_BODY_16, .fontSize = 16, .textColor = {255, 255, 255, 255}})); + } +} +void RenderHeaderButton2(Clay_String text) +{ + CLAY_AUTO_ID( + {.layout = { + .padding = {16, 16, 8, 8}}, + .backgroundColor = {140, 140, 140, 255}, + // .border = { .width = CLAY_BORDER_OUTSIDE(4), .color = {180, 80, 80, 255} }, + .cornerRadius = CLAY_CORNER_RADIUS(5)}) + { + CLAY_TEXT(text, CLAY_TEXT_CONFIG({.fontId = FONT_ID_BODY_16, .fontSize = 16, .textColor = {255, 255, 255, 255}})); + } +} +void RenderHeaderButton3(Clay_String text) +{ + CLAY_AUTO_ID( + {.layout = { + .padding = {16, 16, 8, 8}}, + .backgroundColor = {140, 140, 140, 255}, + .border = {.width = CLAY_BORDER_OUTSIDE(14), .color = {180, 80, 80, 255}}, + .cornerRadius = CLAY_CORNER_RADIUS(0)}) + { + CLAY_TEXT(text, CLAY_TEXT_CONFIG({.fontId = FONT_ID_BODY_16, .fontSize = 16, .textColor = {255, 255, 255, 255}})); + } +} +void RenderHeaderButton4(Clay_String text) +{ + CLAY_AUTO_ID( + {.layout = { + .padding = {16, 16, 8, 8}}, + .backgroundColor = {140, 140, 140, 255}, + .border = {.width = CLAY_BORDER_OUTSIDE(4), .color = {180, 80, 80, 255}}, + .cornerRadius = CLAY_CORNER_RADIUS(5)}) + { + CLAY_TEXT(text, CLAY_TEXT_CONFIG({.fontId = FONT_ID_BODY_16, .fontSize = 16, .textColor = {255, 255, 255, 255}})); + } +} +Clay_RenderCommandArray CreateLayout(void) +{ + Clay_BeginLayout(); + CLAY(CLAY_ID("OuterContainer"), + {.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, .padding = {16, 16, 16, 16}, .childGap = 16}, .backgroundColor = {200, 200, 200, 255}}) + { + CLAY(CLAY_ID("SideBar"), + {.layout = {.layoutDirection = CLAY_TOP_TO_BOTTOM, .sizing = {.width = CLAY_SIZING_FIXED(300), .height = CLAY_SIZING_GROW(0)}, .padding = {16, 16, 16, 16}, .childGap = 16}, .backgroundColor = {150, 150, 255, 255}}) + { + CLAY(CLAY_ID("ProfilePictureOuter"), {.layout = {.sizing = {.width = CLAY_SIZING_GROW(0)}, .padding = {8, 8, 8, 8}, .childGap = 8, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER}}, .backgroundColor = {130, 130, 255, 255}}) + { + CLAY(CLAY_ID("ProfilePicture"), + { + .layout = {.sizing = {.width = CLAY_SIZING_FIXED(60), .height = CLAY_SIZING_FIXED(60)}}, + .image = {.imageData = &g_profilePicture}, + .cornerRadius = {30, 30, 30, 30}, + }) + { + } + CLAY_TEXT(profileText, CLAY_TEXT_CONFIG({.fontSize = 24, .textColor = {0, 0, 0, 255}, .textAlignment = CLAY_TEXT_ALIGN_RIGHT})); + } + CLAY(CLAY_ID("SidebarBlob1"), {.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(50)}}, .backgroundColor = {110, 110, 255, 255}}) {} + CLAY(CLAY_ID("SidebarBlob2"), {.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(50)}}, .backgroundColor = {110, 110, 255, 255}}) {} + CLAY(CLAY_ID("SidebarBlob3"), {.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(50)}}, .backgroundColor = {110, 110, 255, 255}}) {} + CLAY(CLAY_ID("SidebarBlob4"), {.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(50)}}, .backgroundColor = {110, 110, 255, 255}}) {} + } + CLAY(CLAY_ID("RightPanel"), {.layout = {.layoutDirection = CLAY_TOP_TO_BOTTOM, .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, .childGap = 16}}) + { + CLAY_AUTO_ID({.layout = {.sizing = {.width = CLAY_SIZING_GROW(0)}, .childAlignment = {.x = CLAY_ALIGN_X_RIGHT}, .padding = {8, 8, 8, 8}, .childGap = 18}, .backgroundColor = {180, 180, 180, 255}}) + { + RenderHeaderButton1(CLAY_STRING("Header Item 1")); + RenderHeaderButton2(CLAY_STRING("Header Item 2")); + RenderHeaderButton3(CLAY_STRING("Header Item 3")); + RenderHeaderButton4(CLAY_STRING("Header Item 4")); + } + CLAY( + CLAY_ID("MainContent"), + { + .layout = {.layoutDirection = CLAY_TOP_TO_BOTTOM, .padding = {16, 16, 16, 16}, .childGap = 16, .sizing = {.width = CLAY_SIZING_GROW(0)}}, + .backgroundColor = {200, 200, 255, 255}, + .clip = {.vertical = true, .childOffset = Clay_GetScrollOffset()}, + }) + { + CLAY( + CLAY_ID("FloatingContainer"), + { + .layout = {.sizing = {.width = CLAY_SIZING_PERCENT(0.5), .height = CLAY_SIZING_FIXED(300)}, .padding = {16, 16, 16, 16}}, + .backgroundColor = {140, 80, 200, 200}, + .floating = {.attachTo = CLAY_ATTACH_TO_PARENT, .zIndex = 1, .attachPoints = {CLAY_ATTACH_POINT_CENTER_TOP, CLAY_ATTACH_POINT_CENTER_TOP}, .offset = {0, 0}}, + .border = {.width = CLAY_BORDER_OUTSIDE(4), .color = {80, 80, 80, 255}}, + .cornerRadius = {30, 3, 3, 30}, + }) + { + CLAY_TEXT( + CLAY_STRING("I'm an inline floating container."), CLAY_TEXT_CONFIG({.fontSize = 24, .textColor = {255, 255, 255, 255}})); + } + + CLAY_TEXT( + CLAY_STRING("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt."), + CLAY_TEXT_CONFIG({.fontId = FONT_ID_BODY_24, .fontSize = 24, .textColor = {0, 0, 0, 255}})); + + CLAY_TEXT( + CLAY_STRING("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt."), + CLAY_TEXT_CONFIG({.fontId = FONT_ID_BODY_24, .fontSize = 24, .textColor = {0, 0, 0, 255}})); + + CLAY_TEXT(CLAY_STRING("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt."), + CLAY_TEXT_CONFIG({.fontId = FONT_ID_BODY_24, .fontSize = 24, .textColor = {0, 0, 0, 255}})); + + CLAY(CLAY_ID("Photos2"), {.layout = {.childGap = 16, .padding = {16, 16, 16, 16}}, .backgroundColor = {180, 180, 220, (float)(Clay_Hovered() ? 120 : 255)}}) + { + CLAY(CLAY_ID("Picture4"), {.layout = {.sizing = {.width = CLAY_SIZING_FIXED(100), .height = CLAY_SIZING_FIXED(120)}}, .image = {.imageData = &g_window1}}) {} + CLAY(CLAY_ID("Picture5"), {.layout = {.sizing = {.width = CLAY_SIZING_FIXED(100), .height = CLAY_SIZING_FIXED(120)}}, .image = {.imageData = &g_window2}}) {} + CLAY(CLAY_ID("Picture6"), {.layout = {.sizing = {.width = CLAY_SIZING_FIXED(100), .height = CLAY_SIZING_FIXED(120)}}, .image = {.imageData = &g_window3}}) {} + CLAY(CLAY_ID("Picture6.5"), {.layout = {.sizing = {.width = CLAY_SIZING_FIXED(100), .height = CLAY_SIZING_FIXED(120)}}, .image = {.imageData = &g_window4}}) {} + } + + Clay_String cs = {.isStaticallyAllocated = false, .length = g_glInfoTextLen, .chars = g_glInfoText}; + Clay_TextElementConfig glInfoElementConfig = {.fontId = FONT_ID_BODY_24, .fontSize = 24, .textColor = {255, 255, 255, 255}}; + CLAY_TEXT(cs, &glInfoElementConfig); + + CLAY_TEXT( + CLAY_STRING("Faucibus purus in massa tempor nec. Nec ullamcorper sit amet risus nullam eget felis eget nunc. Diam vulputate ut pharetra sit amet aliquam id diam. Lacus suspendisse faucibus interdum posuere lorem. A diam sollicitudin tempor id. Amet massa vitae tortor condimentum lacinia. Aliquet nibh praesent tristique magna."), + CLAY_TEXT_CONFIG({.fontSize = 24, .lineHeight = 60, .textColor = {0, 0, 0, 255}, .textAlignment = CLAY_TEXT_ALIGN_CENTER})); + + CLAY_TEXT(CLAY_STRING("Suspendisse in est ante in nibh. Amet venenatis urna cursus eget nunc scelerisque viverra. Elementum sagittis vitae et leo duis ut diam quam nulla. Enim nulla aliquet porttitor lacus. Pellentesque habitant morbi tristique senectus et. Facilisi nullam vehicula ipsum a arcu cursus vitae.\nSem fringilla ut morbi tincidunt. Euismod quis viverra nibh cras pulvinar mattis nunc sed. Velit sed ullamcorper morbi tincidunt ornare massa. Varius quam quisque id diam vel quam. Nulla pellentesque dignissim enim sit amet venenatis. Enim lobortis scelerisque fermentum dui faucibus in. Pretium viverra suspendisse potenti nullam ac tortor vitae. Lectus vestibulum mattis ullamcorper velit sed. Eget mauris pharetra et ultrices neque ornare aenean euismod elementum. Habitant morbi tristique senectus et. Integer vitae justo eget magna fermentum iaculis eu. Semper quis lectus nulla at volutpat diam. Enim praesent elementum facilisis leo. Massa vitae tortor condimentum lacinia quis vel."), + CLAY_TEXT_CONFIG({.fontSize = 24, .textColor = {0, 0, 0, 255}})); + + CLAY(CLAY_ID("Photos"), {.layout = {.sizing = {.width = CLAY_SIZING_GROW(0)}, .childAlignment = {.x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER}, .childGap = 16, .padding = {16, 16, 16, 16}}, .backgroundColor = {180, 180, 220, 255}}) + { + CLAY(CLAY_ID("Picture2"), {.layout = {.sizing = {.width = CLAY_SIZING_FIXED(120)}}, .aspectRatio = 1, .image = {.imageData = &g_profilePicture}}) {} + CLAY(CLAY_ID("Picture1"), {.layout = {.childAlignment = {.x = CLAY_ALIGN_X_CENTER}, .layoutDirection = CLAY_TOP_TO_BOTTOM, .padding = {8, 8, 8, 8}}, .backgroundColor = {170, 170, 220, 255}}) + { + CLAY(CLAY_ID("ProfilePicture2"), {.layout = {.sizing = {.width = CLAY_SIZING_FIXED(60), .height = CLAY_SIZING_FIXED(60)}}, .image = {.imageData = &g_profilePicture}}) {} + CLAY_TEXT(CLAY_STRING("Image caption below"), CLAY_TEXT_CONFIG({.fontSize = 24, .textColor = {0, 0, 0, 255}})); + } + CLAY(CLAY_ID("Picture3"), {.layout = {.sizing = {.width = CLAY_SIZING_FIXED(120)}}, .aspectRatio = 1, .image = {.imageData = &g_profilePicture}}) {} + } + + CLAY_TEXT( + CLAY_STRING("Amet cursus sit amet dictum sit amet justo donec. Et malesuada fames ac turpis egestas maecenas. A lacus vestibulum sed arcu non odio euismod lacinia. Gravida neque convallis a cras. Dui nunc mattis enim ut tellus elementum sagittis vitae et. Orci sagittis eu volutpat odio facilisis mauris. Neque gravida in fermentum et sollicitudin ac orci. Ultrices dui sapien eget mi proin sed libero. Euismod quis viverra nibh cras pulvinar mattis. Diam volutpat commodo sed egestas egestas. In fermentum posuere urna nec tincidunt praesent semper. Integer eget aliquet nibh praesent tristique magna.\nId cursus metus aliquam eleifend mi in. Sed pulvinar proin gravida hendrerit lectus a. Etiam tempor orci eu lobortis elementum nibh tellus. Nullam vehicula ipsum a arcu cursus vitae. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique senectus. Condimentum lacinia quis vel eros donec ac odio. Mattis pellentesque id nibh tortor id aliquet lectus. Turpis egestas integer eget aliquet nibh praesent tristique. Porttitor massa id neque aliquam vestibulum morbi. Mauris commodo quis imperdiet massa tincidunt nunc pulvinar sapien et. Nunc scelerisque viverra mauris in aliquam sem fringilla. Suspendisse ultrices gravida dictum fusce ut placerat orci nulla.\nLacus laoreet non curabitur gravida arcu ac tortor dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar. Tristique senectus et netus et malesuada fames ac. Nunc aliquet bibendum enim facilisis gravida. Egestas maecenas pharetra convallis posuere morbi leo urna molestie. Sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Ac turpis egestas maecenas pharetra convallis posuere morbi leo urna. Viverra vitae congue eu consequat. Aliquet enim tortor at auctor urna. Ornare massa eget egestas purus viverra accumsan in nisl nisi. Elit pellentesque habitant morbi tristique senectus et netus et malesuada.\nSuspendisse ultrices gravida dictum fusce ut placerat orci nulla pellentesque. Lobortis feugiat vivamus at augue eget arcu. Vitae justo eget magna fermentum iaculis eu. Gravida rutrum quisque non tellus orci. Ipsum faucibus vitae aliquet nec. Nullam non nisi est sit amet. Nunc consequat interdum varius sit amet mattis vulputate enim. Sem fringilla ut morbi tincidunt augue interdum. Vitae purus faucibus ornare suspendisse. Massa tincidunt nunc pulvinar sapien et. Fringilla ut morbi tincidunt augue interdum velit euismod in. Donec massa sapien faucibus et. Est placerat in egestas erat imperdiet. Gravida rutrum quisque non tellus. Morbi non arcu risus quis varius quam quisque id diam. Habitant morbi tristique senectus et netus et malesuada fames ac. Eget lorem dolor sed viverra.\nOrnare massa eget egestas purus viverra. Varius vel pharetra vel turpis nunc eget lorem. Consectetur purus ut faucibus pulvinar elementum. Placerat in egestas erat imperdiet sed euismod nisi. Interdum velit euismod in pellentesque massa placerat duis ultricies lacus. Aliquam nulla facilisi cras fermentum odio eu. Est pellentesque elit ullamcorper dignissim cras tincidunt. Nunc sed id semper risus in hendrerit gravida rutrum. A pellentesque sit amet porttitor eget dolor morbi. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Nisl nunc mi ipsum faucibus vitae aliquet nec ullamcorper. Sed id semper risus in hendrerit gravida. Tincidunt praesent semper feugiat nibh. Aliquet lectus proin nibh nisl condimentum id venenatis a. Enim sit amet venenatis urna cursus eget. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Lacinia quis vel eros donec ac odio tempor orci. Donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Erat pellentesque adipiscing commodo elit at.\nEgestas sed sed risus pretium quam vulputate. Vitae congue mauris rhoncus aenean vel elit scelerisque mauris pellentesque. Aliquam malesuada bibendum arcu vitae elementum. Congue mauris rhoncus aenean vel elit scelerisque mauris. Pellentesque dignissim enim sit amet venenatis urna cursus. Et malesuada fames ac turpis egestas sed tempus urna. Vel fringilla est ullamcorper eget nulla facilisi etiam dignissim. Nibh cras pulvinar mattis nunc sed blandit libero. Fringilla est ullamcorper eget nulla facilisi etiam dignissim. Aenean euismod elementum nisi quis eleifend quam adipiscing vitae proin. Mauris pharetra et ultrices neque ornare aenean euismod elementum. Ornare quam viverra orci sagittis eu. Odio ut sem nulla pharetra diam sit amet nisl suscipit. Ornare lectus sit amet est. Ullamcorper sit amet risus nullam eget. Tincidunt lobortis feugiat vivamus at augue eget arcu dictum.\nUrna nec tincidunt praesent semper feugiat nibh. Ut venenatis tellus in metus vulputate eu scelerisque felis. Cursus risus at ultrices mi tempus. In pellentesque massa placerat duis ultricies lacus sed turpis. Platea dictumst quisque sagittis purus. Cras adipiscing enim eu turpis egestas. Egestas sed tempus urna et pharetra pharetra. Netus et malesuada fames ac turpis egestas integer eget aliquet. Ac turpis egestas sed tempus. Sed lectus vestibulum mattis ullamcorper velit sed. Ante metus dictum at tempor commodo ullamcorper a. Augue neque gravida in fermentum et sollicitudin ac. Praesent semper feugiat nibh sed pulvinar proin gravida. Metus aliquam eleifend mi in nulla posuere sollicitudin aliquam ultrices. Neque gravida in fermentum et sollicitudin ac orci phasellus egestas.\nRidiculus mus mauris vitae ultricies. Morbi quis commodo odio aenean. Duis ultricies lacus sed turpis. Non pulvinar neque laoreet suspendisse interdum consectetur. Scelerisque eleifend donec pretium vulputate sapien nec sagittis aliquam. Volutpat est velit egestas dui id ornare arcu odio ut. Viverra tellus in hac habitasse platea dictumst vestibulum rhoncus est. Vestibulum lectus mauris ultrices eros. Sed blandit libero volutpat sed cras ornare. Id leo in vitae turpis massa sed elementum tempus. Gravida dictum fusce ut placerat orci nulla pellentesque. Pretium quam vulputate dignissim suspendisse in. Nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Risus viverra adipiscing at in tellus. Turpis nunc eget lorem dolor sed viverra ipsum. Senectus et netus et malesuada fames ac. Habitasse platea dictumst vestibulum rhoncus est. Nunc sed id semper risus in hendrerit gravida. Felis eget velit aliquet sagittis id. Eget felis eget nunc lobortis.\nMaecenas pharetra convallis posuere morbi leo. Maecenas volutpat blandit aliquam etiam. A condimentum vitae sapien pellentesque habitant morbi tristique senectus et. Pulvinar mattis nunc sed blandit libero volutpat sed. Feugiat in ante metus dictum at tempor commodo ullamcorper. Vel pharetra vel turpis nunc eget lorem dolor. Est placerat in egestas erat imperdiet sed euismod. Quisque non tellus orci ac auctor augue mauris augue. Placerat vestibulum lectus mauris ultrices eros in cursus turpis. Enim nunc faucibus a pellentesque sit. Adipiscing vitae proin sagittis nisl. Iaculis at erat pellentesque adipiscing commodo elit at imperdiet. Aliquam sem fringilla ut morbi.\nArcu odio ut sem nulla pharetra diam sit amet nisl. Non diam phasellus vestibulum lorem sed. At erat pellentesque adipiscing commodo elit at. Lacus luctus accumsan tortor posuere ac ut consequat. Et malesuada fames ac turpis egestas integer. Tristique magna sit amet purus. A condimentum vitae sapien pellentesque habitant. Quis varius quam quisque id diam vel quam. Est ullamcorper eget nulla facilisi etiam dignissim diam quis. Augue interdum velit euismod in pellentesque massa. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant. Vulputate eu scelerisque felis imperdiet. Nibh tellus molestie nunc non blandit massa. Velit euismod in pellentesque massa placerat. Sed cras ornare arcu dui. Ut sem viverra aliquet eget sit. Eu lobortis elementum nibh tellus molestie nunc non. Blandit libero volutpat sed cras ornare arcu dui vivamus.\nSit amet aliquam id diam maecenas. Amet risus nullam eget felis eget nunc lobortis mattis aliquam. Magna sit amet purus gravida. Egestas purus viverra accumsan in nisl nisi. Leo duis ut diam quam. Ante metus dictum at tempor commodo ullamcorper. Ac turpis egestas integer eget. Fames ac turpis egestas integer eget aliquet nibh. Sem integer vitae justo eget magna fermentum. Semper auctor neque vitae tempus quam pellentesque nec nam aliquam. Vestibulum mattis ullamcorper velit sed. Consectetur adipiscing elit duis tristique sollicitudin nibh. Massa id neque aliquam vestibulum morbi blandit cursus risus.\nCursus sit amet dictum sit amet justo donec enim diam. Egestas erat imperdiet sed euismod. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Habitasse platea dictumst vestibulum rhoncus est pellentesque elit. Duis ultricies lacus sed turpis tincidunt id aliquet risus feugiat. Faucibus ornare suspendisse sed nisi lacus sed viverra. Pretium fusce id velit ut tortor pretium viverra. Fermentum odio eu feugiat pretium nibh ipsum consequat nisl vel. Senectus et netus et malesuada. Tellus pellentesque eu tincidunt tortor aliquam. Aenean sed adipiscing diam donec adipiscing tristique risus nec feugiat. Quis vel eros donec ac odio. Id interdum velit laoreet id donec ultrices tincidunt.\nMassa id neque aliquam vestibulum morbi blandit cursus risus at. Enim tortor at auctor urna nunc id cursus metus. Lorem ipsum dolor sit amet consectetur. At quis risus sed vulputate odio. Facilisis mauris sit amet massa vitae tortor condimentum lacinia quis. Et malesuada fames ac turpis egestas maecenas. Bibendum arcu vitae elementum curabitur vitae nunc sed velit dignissim. Viverra orci sagittis eu volutpat odio facilisis mauris. Adipiscing bibendum est ultricies integer quis auctor elit sed. Neque viverra justo nec ultrices dui sapien. Elementum nibh tellus molestie nunc non blandit massa enim. Euismod elementum nisi quis eleifend quam adipiscing vitae proin sagittis. Faucibus ornare suspendisse sed nisi. Quis viverra nibh cras pulvinar mattis nunc sed blandit. Tristique senectus et netus et. Magnis dis parturient montes nascetur ridiculus mus.\nDolor magna eget est lorem ipsum dolor. Nibh sit amet commodo nulla. Donec pretium vulputate sapien nec sagittis aliquam malesuada. Cras adipiscing enim eu turpis egestas pretium. Cras ornare arcu dui vivamus arcu felis bibendum ut tristique. Mus mauris vitae ultricies leo integer. In nulla posuere sollicitudin aliquam ultrices sagittis orci. Quis hendrerit dolor magna eget. Nisl tincidunt eget nullam non. Vitae congue eu consequat ac felis donec et odio. Vivamus at augue eget arcu dictum varius duis at. Ornare quam viverra orci sagittis.\nErat nam at lectus urna duis convallis. Massa placerat duis ultricies lacus sed turpis tincidunt id aliquet. Est ullamcorper eget nulla facilisi etiam dignissim diam. Arcu vitae elementum curabitur vitae nunc sed velit dignissim sodales. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus. Neque viverra justo nec ultrices dui sapien eget mi proin. Viverra accumsan in nisl nisi scelerisque eu ultrices. Consequat interdum varius sit amet mattis. In aliquam sem fringilla ut morbi. Eget arcu dictum varius duis at. Nulla aliquet porttitor lacus luctus accumsan tortor posuere. Arcu bibendum at varius vel pharetra vel turpis. Hac habitasse platea dictumst quisque sagittis purus sit amet. Sapien eget mi proin sed libero enim sed. Quam elementum pulvinar etiam non quam lacus suspendisse faucibus interdum. Semper viverra nam libero justo. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Et malesuada fames ac turpis egestas maecenas pharetra convallis posuere.\nTurpis egestas sed tempus urna et pharetra pharetra massa. Gravida in fermentum et sollicitudin ac orci phasellus. Ornare suspendisse sed nisi lacus sed viverra tellus in. Fames ac turpis egestas maecenas pharetra convallis posuere. Mi proin sed libero enim sed faucibus turpis. Sit amet mauris commodo quis imperdiet massa tincidunt nunc. Ut etiam sit amet nisl purus in mollis nunc. Habitasse platea dictumst quisque sagittis purus sit amet volutpat consequat. Eget aliquet nibh praesent tristique magna. Sit amet est placerat in egestas erat. Commodo sed egestas egestas fringilla. Enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac. Et molestie ac feugiat sed lectus vestibulum mattis ullamcorper. Dignissim convallis aenean et tortor at risus viverra. Morbi blandit cursus risus at ultrices mi. Ac turpis egestas integer eget aliquet nibh praesent tristique magna.\nVolutpat sed cras ornare arcu dui. Egestas erat imperdiet sed euismod nisi porta lorem mollis aliquam. Viverra justo nec ultrices dui sapien. Amet risus nullam eget felis eget nunc lobortis. Metus aliquam eleifend mi in. Ut eu sem integer vitae. Auctor elit sed vulputate mi sit amet. Nisl nisi scelerisque eu ultrices. Dictum fusce ut placerat orci nulla. Pellentesque habitant morbi tristique senectus et. Auctor elit sed vulputate mi sit. Tincidunt arcu non sodales neque. Mi in nulla posuere sollicitudin aliquam. Morbi non arcu risus quis varius quam quisque id diam. Cras adipiscing enim eu turpis egestas pretium aenean pharetra magna. At auctor urna nunc id cursus metus aliquam. Mauris a diam maecenas sed enim ut sem viverra. Nunc scelerisque viverra mauris in. In iaculis nunc sed augue lacus viverra vitae congue eu. Volutpat blandit aliquam etiam erat velit scelerisque in dictum non."), + CLAY_TEXT_CONFIG({.fontSize = 24, .textColor = {0, 0, 0, 255}})); + } + + CLAY_AUTO_ID({.layout = {.sizing = {.width = CLAY_SIZING_GROW(0)}, .padding = {8, 8, 8, 8}, .childGap = 8}, .backgroundColor = {180, 180, 180, 255}}) + { + char drawCallsText[200]; + int drawCallsTextLen = snprintf(drawCallsText, sizeof(drawCallsText), + "Last frame got: %llu draw calls\n", g_drawCallsDuringLastFrame); + if (drawCallsTextLen < 0) + drawCallsTextLen = 0; + else if ((size_t)drawCallsTextLen >= sizeof(drawCallsText)) + drawCallsTextLen = (int)sizeof(drawCallsText) - 1; + Clay_String cs = {.isStaticallyAllocated = false, .length = (size_t)drawCallsTextLen, .chars = drawCallsText}; + Clay_TextElementConfig drawCallsElementConfig = {.fontId = FONT_ID_BODY_24, .fontSize = 24, .textColor = {255, 255, 255, 255}}; + CLAY_TEXT(cs, &drawCallsElementConfig); + } + CLAY_AUTO_ID({.layout = {.sizing = {.width = CLAY_SIZING_GROW(0)}, .padding = {8, 8, 8, 8}, .childGap = 8}, .backgroundColor = {180, 180, 180, 255}}) + { + Clay_String cs = {.isStaticallyAllocated = false, .length = g_fpsTextLen, .chars = g_fpsText}; + Clay_TextElementConfig fpsElementConfig = {.fontId = FONT_ID_BODY_24, .fontSize = 24, .textColor = {255, 255, 255, 255}}; + CLAY_TEXT(cs, &fpsElementConfig); + } + } + + CLAY( + CLAY_ID("Blob4Floating2"), + { + .floating = {.attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID, .zIndex = 1, .parentId = Clay_GetElementId(CLAY_STRING("SidebarBlob4")).id}, + .backgroundColor = {40, 80, 200, 200}, + .border = {.width = {0, 10, 0, 10}, .color = {0, 0, 0, 80}}, + .cornerRadius = CLAY_CORNER_RADIUS(18), + .layout = { + .padding = {10, 10, 10, 10}, + }, + }) + { + CLAY(CLAY_ID("ScrollContainer"), {.layout = {.sizing = {.height = CLAY_SIZING_FIXED(200)}, .childGap = 2}, .clip = {.vertical = true, .childOffset = Clay_GetScrollOffset()}}) + { + CLAY(CLAY_ID("FloatingContainer2"), {.layout.sizing.height = CLAY_SIZING_GROW(), .floating = {.attachTo = CLAY_ATTACH_TO_PARENT, .zIndex = 1}}) + { + CLAY(CLAY_ID("FloatingContainerInner"), + { + .layout = {.sizing = {.width = CLAY_SIZING_FIXED(300), .height = CLAY_SIZING_GROW()}, .padding = {16, 16, 16, 16}}, + .backgroundColor = {140, 80, 200, 200}, + .border = {.width = CLAY_BORDER_OUTSIDE(4), .color = {80, 80, 80, 255}}, + .cornerRadius = {30, 3, 3, 30}, + }) + { + CLAY_TEXT(CLAY_STRING("I'm an inline floating container."), CLAY_TEXT_CONFIG({.fontSize = 24, .textColor = {255, 255, 0, 255}})); + } + } + CLAY(CLAY_ID("ScrollContainerInner"), {.layout = {.layoutDirection = CLAY_TOP_TO_BOTTOM}, .backgroundColor = {160, 160, 160, 255}}) + { + for (int i = 0; i < 100; i++) + { + RenderDropdownTextItem(i); + } + } + } + } + Clay_ScrollContainerData scrollData = Clay_GetScrollContainerData(Clay_GetElementId(CLAY_STRING("MainContent"))); + if (scrollData.found) + { + CLAY(CLAY_ID("ScrollBar"), + {.floating = { + .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID, + .offset = {.y = -(scrollData.scrollPosition->y / scrollData.contentDimensions.height) * scrollData.scrollContainerDimensions.height}, + .zIndex = 1, + .parentId = Clay_GetElementId(CLAY_STRING("MainContent")).id, + .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_TOP}}}) + { + CLAY( + CLAY_ID("ScrollBarButton"), + {.layout = { + .sizing = {CLAY_SIZING_FIXED(12), CLAY_SIZING_FIXED((scrollData.scrollContainerDimensions.height / scrollData.contentDimensions.height) * scrollData.scrollContainerDimensions.height)}}, + .backgroundColor = Clay_PointerOver(Clay_GetElementId(CLAY_STRING("ScrollBar"))) ? (Clay_Color){100, 100, 140, 150} : (Clay_Color){120, 120, 160, 150}, + .cornerRadius = CLAY_CORNER_RADIUS(6)}) {} + } + } + } + return Clay_EndLayout(); +} + +void loop() +{ + LAST = NOW; + NOW = SDL_GetPerformanceCounter(); + deltaTime = (double)((NOW - LAST) * 1000 / (double)SDL_GetPerformanceFrequency()); + + glClearColor(0.1f, 0.2f, 0.1f, 1.0f); + + Clay_Vector2 scrollDelta = {}; + SDL_Event event; + while (SDL_PollEvent(&event)) + { + switch (event.type) + { + case SDL_QUIT: + { + g_ctx.shouldContinue = false; + } + case SDL_MOUSEWHEEL: + { + scrollDelta.x = event.wheel.x; + scrollDelta.y = event.wheel.y; + break; + } + } + } + + int mouseX = 0; + int mouseY = 0; + Uint32 mouseState = SDL_GetMouseState(&mouseX, &mouseY); + Clay_Vector2 mousePosition = (Clay_Vector2){(float)mouseX, (float)mouseY}; + Clay_SetPointerState(mousePosition, mouseState & SDL_BUTTON(1)); + + Clay_UpdateScrollContainers( + true, + (Clay_Vector2){scrollDelta.x, scrollDelta.y}, + deltaTime); + + SDL_GL_GetDrawableSize(g_ctx.sdlWindow, &g_ctx.screenWidth, &g_ctx.screenHeight); + glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight); + Clay_SetLayoutDimensions((Clay_Dimensions){(float)g_ctx.screenWidth, (float)g_ctx.screenHeight}); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); // Clay renderer is simple and never writes to depth buffer + + Clay_RenderCommandArray cmds = CreateLayout(); + uint64_t drawCalls1 = g_gles3.totalDrawCallsToOpenGl; + Gles3_Render(&g_gles3, cmds, g_stbFonts); + uint64_t drawCalls2 = g_gles3.totalDrawCallsToOpenGl; + g_drawCallsDuringLastFrame = drawCalls2 - drawCalls1; + + // update FPS counter + Uint64 NOW2 = SDL_GetPerformanceCounter(); + + /* FPS based on simulation delta */ + g_timeAccumulator += deltaTime; + g_frameCount++; + + /* wall-clock frame time */ + double frameSeconds = + (double)(NOW2 - NOW) / + (double)SDL_GetPerformanceFrequency(); + + g_wallTimeAccumulator += frameSeconds; + + /* update text ONLY every 5 seconds */ + double measureInterval = 3000.0; + double measuresPerSecond = 1000.0 / measureInterval; + if (g_timeAccumulator >= measureInterval) + { + g_fps = (g_frameCount / g_timeAccumulator) * 1000.0; + + g_avgFrameMs = (g_wallTimeAccumulator / g_frameCount) * 1000.0; + + g_fpsTextLen = (size_t)snprintf( + (char *)g_fpsText, + sizeof(g_fpsText), + "FPS: %.3f | Avg frame: %.3f ms", + g_fps, + g_avgFrameMs); + + g_timeAccumulator = 0.0; + g_wallTimeAccumulator = 0.0; + g_frameCount = 0; + } + + SDL_GL_SwapWindow(g_ctx.sdlWindow); +} + +// Just initializes and spins the animation loop +int main() +{ + initVideo(&g_ctx, 1280, 720); + init(); + + g_ctx.shouldContinue = true; +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(loop, 0, 1); +#else + while (g_ctx.shouldContinue) + { + loop(); + } +#endif +} diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/resources/Roboto-Regular.ttf b/examples/GLES3-SDL2-sidebar-scrolling-container/resources/Roboto-Regular.ttf new file mode 100644 index 0000000..ddf4bfa Binary files /dev/null and b/examples/GLES3-SDL2-sidebar-scrolling-container/resources/Roboto-Regular.ttf differ diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/resources/RobotoMono-Medium.ttf b/examples/GLES3-SDL2-sidebar-scrolling-container/resources/RobotoMono-Medium.ttf new file mode 100644 index 0000000..f6c149a Binary files /dev/null and b/examples/GLES3-SDL2-sidebar-scrolling-container/resources/RobotoMono-Medium.ttf differ diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/resources/millbank.jpeg b/examples/GLES3-SDL2-sidebar-scrolling-container/resources/millbank.jpeg new file mode 100644 index 0000000..d2d8a6b Binary files /dev/null and b/examples/GLES3-SDL2-sidebar-scrolling-container/resources/millbank.jpeg differ diff --git a/examples/GLES3-SDL2-sidebar-scrolling-container/resources/profile-picture.png b/examples/GLES3-SDL2-sidebar-scrolling-container/resources/profile-picture.png new file mode 100644 index 0000000..90ff1fe Binary files /dev/null and b/examples/GLES3-SDL2-sidebar-scrolling-container/resources/profile-picture.png differ diff --git a/examples/GLES3-SDL2-video-demo/.gitignore b/examples/GLES3-SDL2-video-demo/.gitignore new file mode 100644 index 0000000..1ecd022 --- /dev/null +++ b/examples/GLES3-SDL2-video-demo/.gitignore @@ -0,0 +1,2 @@ +/build/ +/website-demo-macos-sdl2* diff --git a/examples/GLES3-SDL2-video-demo/CMakeLists.txt b/examples/GLES3-SDL2-video-demo/CMakeLists.txt new file mode 100644 index 0000000..c3c8d85 --- /dev/null +++ b/examples/GLES3-SDL2-video-demo/CMakeLists.txt @@ -0,0 +1,83 @@ +cmake_minimum_required(VERSION 3.27) +set(CMAKE_C_STANDARD 99) +project(GLES3_SDL2_video_demo C) + + +# ------------------------------------------------- +# FetchContent +# ------------------------------------------------- +include(FetchContent) +set(FETCHCONTENT_QUIET FALSE) + +# ------------------------------------------------- +# STB (header-only) +# ------------------------------------------------- +FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG master +) +FetchContent_MakeAvailable(stb) + +# ------------------------------------------------- +# SDL2 +# ------------------------------------------------- +FetchContent_Declare( + SDL2 + GIT_REPOSITORY "https://github.com/libsdl-org/SDL.git" + GIT_TAG "release-2.30.10" + GIT_PROGRESS TRUE + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(SDL2) + +# ------------------------------------------------- +# Executable +# ------------------------------------------------- +add_executable(GLES3_SDL2_video_demo main.c) +target_compile_options(GLES3_SDL2_video_demo PUBLIC) +target_include_directories(GLES3_SDL2_video_demo + PUBLIC + . # This renderer + ../.. # Clay + ${stb_SOURCE_DIR} # STB header only depencency that does not have its own CMake build + +) + +# ------------------------------------------------- +# Link libraries +# ------------------------------------------------- +target_link_libraries(GLES3_SDL2_video_demo PUBLIC + SDL2::SDL2main + SDL2::SDL2-static +) + +# ------------------------------------------------- +# Platform-specific OpenGL / GLES +# ------------------------------------------------- +find_package(SDL2 REQUIRED) +find_library(OPENGL_FRAMEWORK OpenGL) +target_link_libraries(GLES3_SDL2_video_demo + PRIVATE + ${OPENGL_FRAMEWORK} +) + +# ------------------------------------------------- +# Build flags (kept minimal) +# ------------------------------------------------- +if(MSVC) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") +else() + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") + set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") +endif() + +# ------------------------------------------------- +# Copy resources +# ------------------------------------------------- +add_custom_command( + TARGET GLES3_SDL2_video_demo POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/resources + ${CMAKE_CURRENT_BINARY_DIR}/resources +) diff --git a/examples/GLES3-SDL2-video-demo/Makefile.emscripten b/examples/GLES3-SDL2-video-demo/Makefile.emscripten new file mode 100644 index 0000000..f56d8e4 --- /dev/null +++ b/examples/GLES3-SDL2-video-demo/Makefile.emscripten @@ -0,0 +1,33 @@ +# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab: + +# +# PROGRAM COMPONENTS +# + +CXX = emcc + +CXXFLAGS = -std=c99 +CXXFLAGS += -O0 +CXXFLAGS += -I../.. +CXXFLAGS += -I./build/_deps/stb-src + +LDLIBS += -s USE_ZLIB=1 +LDLIBS += -s USE_SDL=2 +LDLIBS += -s FULL_ES2=1 -s USE_WEBGL2=1 +LDLIBS += -s ALLOW_MEMORY_GROWTH=1 -s GL_UNSAFE_OPTS=0 +LDLIBS += -s STACK_SIZE=2048kb +LDLIBS += -s EXPORTED_FUNCTIONS=['_main'] +LDLIBS += -s ASSERTIONS=1 -s SAFE_HEAP=1 +LDLIBS += --preload-file $(PWD)/resources/Roboto-Regular.ttf@resources/Roboto-Regular.ttf + +main: + mkdir -p build/emscripten + time $(CXX) $(CXXFLAGS) \ + $(PWD)/main.c \ + $(LDLIBS) -o build/emscripten/index.html + +test: + make -f Makefile.emscripten main \ + && (cd build/emscripten && python3 -mhttp.server) + +.PHONY: main diff --git a/examples/GLES3-SDL2-video-demo/Makefile.macos b/examples/GLES3-SDL2-video-demo/Makefile.macos new file mode 100644 index 0000000..6db93d0 --- /dev/null +++ b/examples/GLES3-SDL2-video-demo/Makefile.macos @@ -0,0 +1,28 @@ +# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab: + +CXX = clang + +CXXFLAGS = -std=c99 +CXXFLAGS += -g -O0 -fno-omit-frame-pointer +CXXFLAGS += -ferror-limit=1 +CXXFLAGS += -I../.. +CXXFLAGS += -I./build/_deps/stb-src +CXXFLAGS += -DGL_SILENCE_DEPRECATION + +# SDL2 (Homebrew) +CXXFLAGS += -I$(shell brew --prefix sdl2)/include/SDL2 +LDLIBS += -L$(shell brew --prefix sdl2)/lib -lSDL2 + +# macOS system frameworks (OpenGL needs these) +LDLIBS += -framework OpenGL +LDLIBS += -framework Cocoa +LDLIBS += -framework IOKit +LDLIBS += -framework CoreVideo + +main: + mkdir -p build + + time $(CXX) $(CXXFLAGS) \ + $(PWD)/main.c \ + $(LDLIBS) \ + -o build/website-demo-macos-sdl2 diff --git a/examples/GLES3-SDL2-video-demo/README.md b/examples/GLES3-SDL2-video-demo/README.md new file mode 100644 index 0000000..d3f7b86 --- /dev/null +++ b/examples/GLES3-SDL2-video-demo/README.md @@ -0,0 +1,38 @@ +GLES3 Renderer Video Demo (Using SDL2) +====================================== + +This directory contains a standard Video-Demo example +using work-in-progress GLES3 renderer. +While it still needs refinement, the renderer is already functional and demonstrates the core rendering pipeline. + +Current features + +- Supports all draw commands except custom. +- In the best-case scenario (no clipping): +- All quad-based commands (Rectangle, Image, Border) are rendered in a single draw call. +- All glyphs belonging to the same font are rendered in one instanced draw call. +- When clipping (scissoring) is used: +- The renderer flushes draw calls before and after each scissor region. +- Supports up to 4 fonts and 4 image textures. +- Image textures may also be used as texture atlases. +- Custom UserData provides per-image UV coordinates, allowing multiple images to share a single OpenGL texture. +- Uses stb_image.h and stb_truetype.h as single-header dependencies for asset loading. +- The loading layer is modular and can be replaced with a different asset pipeline if needed. + +Currently builds on: +- Emscripten +- clang++ / macOS +- CMake support is not available yet. + +Windowing and platform support + +This example uses SDL2, but the renderer is framework agnostic. + +For sake of example you can also build it with +hand-crafted Makefile.macos + + make -f Makefile.emscripten test + +and then navigate to http://localhost:8080 +On Emscripten it works well. + diff --git a/examples/GLES3-SDL2-video-demo/main.c b/examples/GLES3-SDL2-video-demo/main.c new file mode 100644 index 0000000..244049c --- /dev/null +++ b/examples/GLES3-SDL2-video-demo/main.c @@ -0,0 +1,198 @@ +#include + +#define STB_IMAGE_IMPLEMENTATION +#define STB_TRUETYPE_IMPLEMENTATION +#define CLAY_IMPLEMENTATION +#define CLAY_RENDERER_GLES3_IMPLEMENTATION + +#include + +#include "../../renderers/GLES3/clay_renderer_gles3.h" +#include "../shared-layouts/clay-video-demo.c" +#include "../../renderers/GLES3/clay_renderer_gles3_loader_stb.c" + +typedef struct VideoCtx +{ + int shouldContinue; + SDL_Window *sdlWindow; + SDL_GLContext sdlContext; + int screenWidth, screenHeight; +} VideoCtx; + +VideoCtx g_ctx; + +static int initVideo(VideoCtx *ctx, const int initialWidth, const int initialHeight) +{ + SDL_Init(SDL_INIT_VIDEO); + +#if defined(__EMSCRIPTEN__) + // OpenGL ES 3 profile + SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1"); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); +#else + // Apple MacOs will use it own legacy desktop GL instead + // I know, I lied, I said this was an GLES3 + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); +#endif + + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); + + g_ctx.sdlWindow = SDL_CreateWindow( + "SDL2 GLES3", + SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, + initialWidth, + initialHeight, + SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN + ); + g_ctx.sdlContext = SDL_GL_CreateContext(g_ctx.sdlWindow); + + SDL_ShowWindow(g_ctx.sdlWindow); + SDL_Delay(1); + SDL_GL_GetDrawableSize(g_ctx.sdlWindow, &g_ctx.screenWidth, &g_ctx.screenHeight); + glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight); + + glEnable(GL_BLEND); + // Enables blending, which allows transparent textures to be rendered properly. + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // Sets the blending function. + // - `GL_SRC_ALPHA`: Uses the alpha value of the source (texture or color). + // - `GL_ONE_MINUS_SRC_ALPHA`: Makes the destination color blend with the background based on alpha. + // This is commonly used for standard transparency effects. + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable(GL_DEPTH_TEST); + // Enables depth testing, ensuring that objects closer to the camera are drawn in front of those farther away. + // This prevents objects from rendering incorrectly based on draw order. + + return 1; +} + +void My_ErrorHandler(Clay_ErrorData errorData) +{ + printf("[ClaY ErroR] %s", errorData.errorText.chars); +} + +Stb_FontData g_stbFonts[MAX_FONTS]; // Fonts userData +Gles3_Renderer g_gles3; // The renderer itself + +Uint64 NOW = 0; +Uint64 LAST = 0; +double deltaTime = 0; + +// is executed before everything +void init() +{ + size_t clayRequiredMemory = Clay_MinMemorySize(); + g_gles3.clayMemory = (Clay_Arena){ + .capacity = clayRequiredMemory, + .memory = (char *)malloc(clayRequiredMemory), + }; + Clay_Context *clayCtx = Clay_Initialize( + g_gles3.clayMemory, + (Clay_Dimensions){ + .width = (float)g_ctx.screenWidth, + .height = (float)g_ctx.screenHeight, + }, + (Clay_ErrorHandler){ + .errorHandlerFunction = My_ErrorHandler, + }); + + // Note that MeasureText has to be set after the Context is set! + Clay_SetCurrentContext(clayCtx); + Clay_SetMeasureTextFunction(Stb_MeasureText, &g_stbFonts); + Gles3_SetRenderTextFunction(&g_gles3, Stb_RenderText, &g_stbFonts); + + Gles3_Initialize(&g_gles3, 4096); + + int atlasW = 1024; + int atlasH = 1024; + if (!Stb_LoadFont( + &g_gles3.fontTextures[0], + &g_stbFonts[0], + "resources/Roboto-Regular.ttf", + 24.0f, // bake pixel height + atlasW, + atlasH)) + abort(); + + Clay_SetDebugModeEnabled(true); +} + +void loop() +{ + + glClearColor(0.1f, 0.2f, 0.1f, 1.0f); + + Clay_Vector2 scrollDelta = {}; + SDL_Event event; + while (SDL_PollEvent(&event)) + { + switch (event.type) + { + case SDL_QUIT: + { + g_ctx.shouldContinue = false; + } + case SDL_MOUSEWHEEL: + { + scrollDelta.x = event.wheel.x; + scrollDelta.y = event.wheel.y; + break; + } + } + } + LAST = NOW; + NOW = SDL_GetPerformanceCounter(); + deltaTime = (double)((NOW - LAST) * 1000 / (double)SDL_GetPerformanceFrequency()); + + int mouseX = 0; + int mouseY = 0; + Uint32 mouseState = SDL_GetMouseState(&mouseX, &mouseY); + Clay_Vector2 mousePosition = (Clay_Vector2){(float)mouseX, (float)mouseY}; + Clay_SetPointerState(mousePosition, mouseState & SDL_BUTTON(1)); + + Clay_UpdateScrollContainers( + true, + (Clay_Vector2){scrollDelta.x, scrollDelta.y}, + deltaTime); + + SDL_GL_GetDrawableSize(g_ctx.sdlWindow, &g_ctx.screenWidth, &g_ctx.screenHeight); + glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight); + Clay_SetLayoutDimensions((Clay_Dimensions){(float)g_ctx.screenWidth, (float)g_ctx.screenHeight}); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); // Clay renderer is simple and never writes to depth buffer + + ClayVideoDemo_Data data = ClayVideoDemo_Initialize(); + Clay_RenderCommandArray cmds = ClayVideoDemo_CreateLayout(&data); + + Gles3_Render(&g_gles3, cmds, g_stbFonts); + + SDL_GL_SwapWindow(g_ctx.sdlWindow); +} + +// Just initializes and spins the animation loop +int main() +{ + initVideo(&g_ctx, 1280, 720); + init(); + + g_ctx.shouldContinue = true; +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(loop, 0, 1); +#else + while (g_ctx.shouldContinue) + { + loop(); + } +#endif +} \ No newline at end of file diff --git a/examples/GLES3-SDL2-video-demo/resources/Roboto-Regular.ttf b/examples/GLES3-SDL2-video-demo/resources/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/examples/GLES3-SDL2-video-demo/resources/Roboto-Regular.ttf differ diff --git a/examples/shared-layouts/clay-video-demo.c b/examples/shared-layouts/clay-video-demo.c index 8f9d2c8..5ab9d61 100644 --- a/examples/shared-layouts/clay-video-demo.c +++ b/examples/shared-layouts/clay-video-demo.c @@ -219,7 +219,7 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) { SidebarClickData *clickData = (SidebarClickData *)(data->frameArena.memory + data->frameArena.offset); *clickData = (SidebarClickData) { .requestedDocumentIndex = i, .selectedDocumentIndex = &data->selectedDocumentIndex }; data->frameArena.offset += sizeof(SidebarClickData); - CLAY_AUTO_ID({ .layout = sidebarButtonLayout, .backgroundColor = (Clay_Color) { 120, 120, 120, Clay_Hovered() ? 120 : 0 }, .cornerRadius = CLAY_CORNER_RADIUS(8) }) { + CLAY_AUTO_ID({ .layout = sidebarButtonLayout, .backgroundColor = (Clay_Color) { 120, 120, 120, (float)(Clay_Hovered() ? 120 : 0) }, .cornerRadius = CLAY_CORNER_RADIUS(8) }) { Clay_OnHover(HandleSidebarInteraction, clickData); CLAY_TEXT(document.title, CLAY_TEXT_CONFIG({ .fontId = FONT_ID_BODY_16, diff --git a/renderers/GLES3/clay_renderer_gles3.h b/renderers/GLES3/clay_renderer_gles3.h new file mode 100644 index 0000000..4792000 --- /dev/null +++ b/renderers/GLES3/clay_renderer_gles3.h @@ -0,0 +1,822 @@ +#ifndef CLAY_RENDERER_GLES3_H +#define CLAY_RENDERER_GLES3_H + +// There may be custom header customizations, very client specific +// let client indicate that they manage headers by setting GLSL_VERSION +#ifndef GLSL_VERSION +#if defined(__EMSCRIPTEN__) +#include +#include +#include +#define GLSL_VERSION "#version 300 es" +#else +// Only apple computers now sorry +// That means it is not really GLES3 but desktop OpenGL 3 +// Luckily, it is compatible with GLES3 +#include +#define GLSL_VERSION "#version 330 core" +#endif +#endif + +#define MAX_IMAGES 4 +#define MAX_FONTS 4 + +/* + * Instanced rendering for Rects/Images/Borders + * will use this data + * Note, it needs to be padded to 4 floats + * Draws: + * - One rectangular with possibly rounded corner + * - And possibly with a hole inside (with rounded edges too, if corners are rounded) + * - It could also draw a picture with alsoe rounded corner + */ +typedef struct RectInstance +{ + float x, y, w, h; // 4 Draw where on screen + float u0, v0, u1, v1; // 4 Atlas region + float r, g, b, a; // 4 Color + float radiusTL, radiusTR; // 2 Corner rounding + float radiusBL, radiusBR; // 2 + float borderL, borderR; // 2 Border widths + float borderT, borderB; // 2 + float texToUse; // 1 Texture atlas to take an image from (1-4) + float pad[3]; // 3 +} RectInstance; + +/* + * Struct for glyph instanced rendering + * Each glyph consists of 6 vertexes (to make 2 triangle of a quad) + */ +typedef struct GlyphVtx +{ + float x, y; // To draw Where + float u, v; // To draw What + float r, g, b, a; // Text color + float atlasTexUnit; // Shader will have all samples loaded but this will point which to use + float pad[3]; // 3 +} GlyphVtx; + +typedef struct Gles3_GlyphVtxArray +{ + GlyphVtx *instData; + int capacity; + int count; +} Gles3_GlyphVtxArray; + +typedef struct Gles3_QuadInstanceArray +{ + RectInstance *instData; // packed per-instance floats + int capacity; // how many instances it can hold + int count; // how many instances does it actually hold +} Gles3_QuadInstanceArray; + +typedef struct Gles3_ImageConfig +{ + int textureToUse; + float u0, v0; + float u1, v1; +} Gles3_ImageConfig; + +#ifndef CLAY_RENDERER_GLES3_IMPLEMENTATION +typedef struct Gles3_Renderer Gles3_Renderer; +#endif + +#ifdef CLAY_RENDERER_GLES3_IMPLEMENTATION + +#include +#include +#include + +#include "clay_renderer_gles3.h" + +enum +{ + ATTR_QUAD_POS = 0, + ATTR_QUAD_RECT = 1, + ATTR_QUAD_COLOR = 2, + ATTR_QUAD_UV = 3, + ATTR_QUAD_RAD = 4, + ATTR_QUAD_BORDER = 5, + ATTR_QUAD_TEX = 6, +}; + +enum +{ + ATTR_GLYPH_POS = 0, + ATTR_GLYPH_UV = 1, + ATTR_GLYPH_COLOR = 2, + ATTR_GLYPH_TEX = 3, +}; + +/* + * rendering + */ + +const char *GLES3_QUAD_VERTEX_SHADER = + GLSL_VERSION + "\n" + "precision mediump float;\n" + "layout(location = 0) in vec2 aPos; // unit quad (0..1)\n" + "layout(location = 1) in vec4 aRect; // x,y,w,h (pixels)\n" + "layout(location = 3) in vec4 aUV; // u0,v0,u1,v1\n" + "layout(location = 2) in vec4 aColor; // rgba\n" + "layout(location = 4) in vec4 aCornerRadii;\n" + "layout(location = 5) in vec4 aBorderWidths;\n" + "layout(location = 6) in float aTexSlot;\n" + "uniform vec2 uScreen; // screen size in pixels\n" + "out vec2 vPos;\n" + "out vec4 vRect;\n" + "out vec4 vColor;\n" + "out vec2 vUV;\n" + "out vec4 vCornerRadii;\n" + "out vec4 vBorderWidths;\n" + "out float vTexSlot;\n" + "void main() {\n" + " vec2 pos = vec2(aPos.x * aRect.z + aRect.x, aPos.y * aRect.w + aRect.y);\n" + " vec2 ndc = pos / uScreen * 2.0 - 1.0; // ndc.y increases up; pos y increases down (we will inve\n" + " ndc.y = -ndc.y;\n" + " gl_Position = vec4(ndc, 0.0, 1.0);\n" + " vPos = aPos;\n" + " vRect = aRect;\n" + " vColor = aColor;\n" + " vUV = mix(aUV.xy, aUV.zw, aPos);\n" + " vCornerRadii = aCornerRadii;\n" + " vBorderWidths = aBorderWidths;\n" + " vTexSlot = aTexSlot;\n" + "}\n"; + +const char *GLES3_QUAD_FRAGMENT_SHADER = + GLSL_VERSION + "\n" + "precision mediump float;\n" + "in vec2 vPos;\n" + "in vec4 vRect;\n" + "in vec4 vColor;\n" + "in vec2 vUV;\n" + "in vec4 vCornerRadii;\n" + "in vec4 vBorderWidths;\n" + "in float vTexSlot;\n" + "uniform sampler2D uTex0;\n" + "uniform sampler2D uTex1;\n" + "uniform sampler2D uTex2;\n" + "uniform sampler2D uTex3;\n" + "out vec4 frag;\n" + "void main() {\n" + " // Pixel coordinates in pixel space\n" + " vec2 pix = vRect.xy + vPos * vRect.zw;\n" + " float x0 = vRect.x;\n" + " float y0 = vRect.y;\n" + " float w = vRect.z;\n" + " float h = vRect.w;\n" + " // Local position inside the rectangle (0..w, 0..h)\n" + " vec2 local = pix - vec2(x0, y0);\n" + " // Original corner radii\n" + " float tl = vCornerRadii.x;\n" + " float tr = vCornerRadii.y;\n" + " float bl = vCornerRadii.z;\n" + " float br = vCornerRadii.w;\n" + " // Border thicknesses\n" + " float L = vBorderWidths.x;\n" + " float R = vBorderWidths.y;\n" + " float T = vBorderWidths.z;\n" + " float B = vBorderWidths.w;\n" + " bool CLAY_BORDERS_ARE_INSET = true; // it is true\n" + " bool isBorder = (L > 0.0 || R > 0.0 || T > 0.0 || B > 0.0);\n" + " float outerAlpha = 1.0;\n" + " // If it is not a border but rect or image, then it only has outer border what is provided\n" + " // Otherwise it increases the outter border, but the provided borde is the ineer border\n" + " // I think is better not increase rounding radius when that radius is smaller than border thickness\n" + " float outter_tl;\n" + " float outter_tr;\n" + " float outter_bl;\n" + " float outter_br;\n" + " if (CLAY_BORDERS_ARE_INSET) {\n" + " // Actural behaviour\n" + " outter_tl = tl;\n" + " outter_tr = tr;\n" + " outter_bl = bl;\n" + " outter_br = br;\n" + " tl = (tl > min(T, L)) ? tl - min(T, L) : tl;\n" + " tr = (tr > min(T, R)) ? tr - min(T, R) : tr;\n" + " bl = (bl > min(B, L)) ? bl - min(B, L) : bl;\n" + " br = (br > min(B, R)) ? br - min(B, R) : br;\n" + " } else {\n" + " // Hypothetical behaviour\n" + " outter_tl = (tl > min(T, L)) ? tl + min(T, L) : tl;\n" + " outter_tr = (tr > min(T, R)) ? tr + min(T, R) : tr;\n" + " outter_bl = (bl > min(B, L)) ? bl + min(B, L) : bl;\n" + " outter_br = (br > min(B, R)) ? br + min(B, R) : br;\n" + " }\n" + " if (outter_tl > 0.0 && local.x < outter_tl && local.y < outter_tl)\n" + " outerAlpha = step(length(local - vec2(outter_tl, outter_tl)), outter_tl);\n" + " if (outter_tr > 0.0 && local.x > w - outter_tr && local.y < outter_tr)\n" + " outerAlpha *= step(length(local - vec2(w - outter_tr, outter_tr)), outter_tr);\n" + " if (outter_bl > 0.0 && local.x < outter_bl && local.y > h - outter_bl)\n" + " outerAlpha *= step(length(local - vec2(outter_bl, h - outter_bl)), outter_bl);\n" + " if (outter_br > 0.0 && local.x > w - outter_br && local.y > h - outter_br)\n" + " outerAlpha *= step(length(local - vec2(w - outter_br, h - outter_br)), outter_br);\n" + " if (outerAlpha < 0.5)\n" + " discard;\n" + " // -------- Border logic --------\n" + " if (isBorder) {\n" + " float iw = w - L - R;\n" + " float ih = h - T - B;\n" + " vec2 innerLocal = local - vec2(L, T);\n" + " // Check if pixel is inside inner rounded rect\n" + " bool insideInner = true;\n" + " if (tl > 0.0 && innerLocal.x < tl && innerLocal.y < tl)\n" + " insideInner = (length(innerLocal - vec2(tl, tl)) <= tl);\n" + " if (tr > 0.0 && innerLocal.x > iw - tr && innerLocal.y < tr)\n" + " insideInner = insideInner && (length(innerLocal - vec2(iw - tr, tr)) <= tr);\n" + " // Bottom-left\n" + " if (bl> 0.0 && innerLocal.x < bl && innerLocal.y > ih - bl) \n" + " insideInner = insideInner && (length(innerLocal - vec2(bl, ih - bl)) <= bl);\n" + " // Bottom-right\n" + " if (br > 0.0 && innerLocal.x > iw - br && innerLocal.y > ih - br)\n" + " insideInner = insideInner && (length(innerLocal - vec2(iw - br, ih - br)) <= br);\n" + " // Discard pixels inside inner rounded rect\n" + " if (insideInner && innerLocal.x >= 0.0 && innerLocal.x <= iw && innerLocal.y >= 0.0 && innerLocal.y <= ih)\n" + " discard;\n" + " frag = vColor;\n" + " return;\n" + " }\n" + " // -------- Non-border rectangle or image --------\n" + " if (vTexSlot < 0.0) {\n" + " frag = vColor;\n" + " } else {\n" + " int slot = int(vTexSlot + 0.5);\n" + " if (slot == 0) frag = texture(uTex0, vUV);\n" + " if (slot == 1) frag = texture(uTex1, vUV);\n" + " if (slot == 2) frag = texture(uTex2, vUV);\n" + " if (slot == 3) frag = texture(uTex3, vUV);\n" + " }\n" + "}\n"; + +const char *GLES3_TEXT_VERTEX_SHADER = + GLSL_VERSION + "\n" + "precision mediump float;\n" + "layout(location = 0) in vec2 aPos;\n" + "layout(location = 1) in vec2 aUV;\n" + "layout(location = 2) in vec4 aColor;\n" + "layout(location = 3) in float aTexSlot;\n" + "uniform vec2 uScreen;\n" + "out vec2 vUV;\n" + "out vec4 vColor;\n" + "out float vTexSlot;\n" + "void main() {\n" + " vec2 ndc = (aPos / uScreen) * 2.0 - 1.0;\n" + " gl_Position = vec4(ndc * vec2(1.0, -1.0), 0.0, 1.0);\n" + " vUV = aUV;\n" + " vColor = aColor;\n" + " vTexSlot = aTexSlot;\n" + "}\n"; + +const char *GLES3_TEXT_FRAGMENT_SHADER = + GLSL_VERSION + "\n" + "precision mediump float;\n" + "in vec2 vUV;\n" + "in vec4 vColor;\n" + "in float vTexSlot;\n" + "uniform sampler2D uTex0;\n" + "uniform sampler2D uTex1;\n" + "uniform sampler2D uTex2;\n" + "uniform sampler2D uTex3;\n" + "out vec4 fragColor;\n" + "void main() {\n" + " int slot = int(vTexSlot + 0.5);\n" + " float coverage;\n" + " if (slot == 0) coverage = texture(uTex0, vUV).r;\n" + " if (slot == 1) coverage = texture(uTex1, vUV).r;\n" + " if (slot == 2) coverage = texture(uTex2, vUV).r;\n" + " if (slot == 3) coverage = texture(uTex3, vUV).r;\n" + " fragColor = vec4(vColor.rgb, vColor.a * coverage);\n" + "} \n"; + +/** + * This renderer accumulates all quads and glyphs of every draw coommand + * in their array, and flushes them in just 2 instanced draw calls to OpenGL + */ +typedef struct Gles3_Renderer +{ + Clay_Arena clayMemory; + + // It is super important keep track on the performance of this renderer: + uint64_t totalDrawCallsToOpenGl; + + float screenWidth; + float screenHeight; + + /* Quads rendering */ + GLuint quadVAO; + GLuint quadVBO; + GLuint quadInstanceVBO; + GLuint quadShaderId; + GLuint imageTextures[MAX_IMAGES]; + Gles3_QuadInstanceArray quadInstanceArray; // Each instance is one quad + + /* Fonts rendering */ + GLuint textVAO; + GLuint textVBO; + GLuint textShader; + GLuint fontTextures[MAX_FONTS]; + Gles3_GlyphVtxArray glyphVtxArray; // Instance data: every vertex is an element, + // 6 elements per each instance + + // Text renderer is delegated to external function, which is supposed + // to add glyph data based on passed render text command + void (*renderTextFunction)( + Clay_RenderCommand *cmd, // Will be always of CLAY_RENDER_COMMAND_TYPE_TEXT + Gles3_GlyphVtxArray *accum, // 6 vertices need to be added to this array + void *userData // Fonts pallete + ); +} Gles3_Renderer; + +static GLuint Gles3__CompileShader(GLenum type, const char *source) +{ + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, NULL); + glCompileShader(shader); + + GLint success; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (!success) + { + char infoLog[512]; + glGetShaderInfoLog(shader, 512, NULL, infoLog); + + printf("ERROR::SHADER::COMPILATION_FAILED\n"); + printf("SHADER SOURCE:\n%s\n", source); + printf("SHADER TYPE: "); + if (type == GL_VERTEX_SHADER) + printf("Vertex Shader"); + else if (type == GL_FRAGMENT_SHADER) + printf("Fragment Shader"); + else + printf("Unknown"); + printf("\nSHADER COMPILATION ERROR:\n%s\n", infoLog); + abort(); + } + return shader; +} + +GLuint Gles3__CreateShaderProgram( + const char *vertexShaderSource, + const char *fragmentShaderSource) +{ + GLuint vertexShader = + Gles3__CompileShader(GL_VERTEX_SHADER, vertexShaderSource); + GLuint fragmentShader = + Gles3__CompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); + + GLuint shaderProgram = glCreateProgram(); + glAttachShader(shaderProgram, vertexShader); + glAttachShader(shaderProgram, fragmentShader); + glLinkProgram(shaderProgram); + + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + + return shaderProgram; +} + +void Gles3_Initialize(Gles3_Renderer *renderer, int maxInstances) +{ + renderer->totalDrawCallsToOpenGl = 0; + // compile shader + renderer->quadShaderId = Gles3__CreateShaderProgram( + GLES3_QUAD_VERTEX_SHADER, GLES3_QUAD_FRAGMENT_SHADER); + + glUseProgram(renderer->quadShaderId); + glUniform1i(glGetUniformLocation(renderer->quadShaderId, "uTex0"), 0); + glUniform1i(glGetUniformLocation(renderer->quadShaderId, "uTex1"), 1); + glUniform1i(glGetUniformLocation(renderer->quadShaderId, "uTex2"), 2); + glUniform1i(glGetUniformLocation(renderer->quadShaderId, "uTex3"), 3); + + // create unit quad VBO (0..1) + const float quadVerts[8] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f}; + glGenVertexArrays(1, &renderer->quadVAO); + glBindVertexArray(renderer->quadVAO); + + glGenBuffers(1, &renderer->quadVBO); + glBindBuffer(GL_ARRAY_BUFFER, renderer->quadVBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW); + + // attribute 0: aPos (vec2), per-vertex + glEnableVertexAttribArray(ATTR_QUAD_POS); + glVertexAttribPointer(ATTR_QUAD_POS, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0); + glVertexAttribDivisor(ATTR_QUAD_POS, 0); + + // create instance buffer big enough + Gles3_QuadInstanceArray *quads = &renderer->quadInstanceArray; + quads->capacity = maxInstances; + quads->instData = + (RectInstance *)malloc(sizeof(RectInstance) * quads->capacity); + quads->count = 0; + + glGenBuffers(1, &renderer->quadInstanceVBO); + glBindBuffer(GL_ARRAY_BUFFER, renderer->quadInstanceVBO); + glBufferData(GL_ARRAY_BUFFER, + sizeof(RectInstance) * quads->capacity, + NULL, + GL_DYNAMIC_DRAW); + + // set up instance attributes + GLsizei stride = sizeof(RectInstance); + + glEnableVertexAttribArray(ATTR_QUAD_RECT); + glVertexAttribPointer(ATTR_QUAD_RECT, 4, GL_FLOAT, GL_FALSE, + stride, (void *)offsetof(RectInstance, x)); + glVertexAttribDivisor(ATTR_QUAD_RECT, 1); + + glEnableVertexAttribArray(ATTR_QUAD_COLOR); + glVertexAttribPointer(ATTR_QUAD_COLOR, 4, GL_FLOAT, GL_FALSE, + stride, (void *)offsetof(RectInstance, r)); + glVertexAttribDivisor(ATTR_QUAD_COLOR, 1); + + glEnableVertexAttribArray(ATTR_QUAD_UV); + glVertexAttribPointer(ATTR_QUAD_UV, 4, GL_FLOAT, GL_FALSE, + stride, (void *)offsetof(RectInstance, u0)); + glVertexAttribDivisor(ATTR_QUAD_UV, 1); + + glEnableVertexAttribArray(ATTR_QUAD_RAD); + glVertexAttribPointer(ATTR_QUAD_RAD, 4, GL_FLOAT, GL_FALSE, + stride, (void *)offsetof(RectInstance, radiusTL)); + glVertexAttribDivisor(ATTR_QUAD_RAD, 1); + + glEnableVertexAttribArray(ATTR_QUAD_BORDER); + glVertexAttribPointer(ATTR_QUAD_BORDER, 4, GL_FLOAT, GL_FALSE, + stride, (void *)offsetof(RectInstance, borderL)); + glVertexAttribDivisor(ATTR_QUAD_BORDER, 1); + + glEnableVertexAttribArray(ATTR_QUAD_TEX); + glVertexAttribPointer(ATTR_QUAD_TEX, 1, GL_FLOAT, GL_FALSE, + stride, (void *)offsetof(RectInstance, texToUse)); + glVertexAttribDivisor(ATTR_QUAD_TEX, 1); + + glBindVertexArray(1); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + // Ok now we will initialize text! + Gles3_GlyphVtxArray *gVerts = &renderer->glyphVtxArray; + + // configure capacity + gVerts->capacity = maxInstances; + gVerts->count = 0; + + // allocate CPU-side vertex buffer: 6 vertices per glyph + gVerts->instData = (GlyphVtx *)malloc(sizeof(GlyphVtx) * 6 * gVerts->capacity); + if (!gVerts->instData) + { + fprintf(stderr, "Failed to allocate glyph_vertices\n"); + gVerts->capacity = 0; + } + + // create VAO/VBO for text rendering + glGenVertexArrays(1, &renderer->textVAO); + glBindVertexArray(renderer->textVAO); + + glGenBuffers(1, &renderer->textVBO); + glBindBuffer(GL_ARRAY_BUFFER, renderer->textVBO); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GlyphVtx) * 6 * gVerts->capacity, + NULL, + GL_DYNAMIC_DRAW); + + GLsizei gv_stride = sizeof(GlyphVtx); + + glEnableVertexAttribArray(ATTR_GLYPH_POS); + glVertexAttribPointer(ATTR_GLYPH_POS, 2, GL_FLOAT, GL_FALSE, gv_stride, (void *)(offsetof(GlyphVtx, x))); + + glEnableVertexAttribArray(ATTR_GLYPH_UV); + glVertexAttribPointer(ATTR_GLYPH_UV, 2, GL_FLOAT, GL_FALSE, gv_stride, (void *)(offsetof(GlyphVtx, u))); + + glEnableVertexAttribArray(ATTR_GLYPH_COLOR); + glVertexAttribPointer(ATTR_GLYPH_COLOR, 4, GL_FLOAT, GL_FALSE, gv_stride, (void *)(offsetof(GlyphVtx, r))); + + glEnableVertexAttribArray(ATTR_GLYPH_TEX); + glVertexAttribPointer(ATTR_GLYPH_TEX, 1, GL_FLOAT, GL_FALSE, gv_stride, (void *)(offsetof(GlyphVtx, atlasTexUnit))); + + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + renderer->textShader = Gles3__CreateShaderProgram( + GLES3_TEXT_VERTEX_SHADER, GLES3_TEXT_FRAGMENT_SHADER); + glUseProgram(renderer->textShader); + + // Link sampler uniforms in the text shader to the correct texture units. + // Each uniform tells the shader which unit to read from. + glUniform1i(glGetUniformLocation(renderer->textShader, "uTex0"), 0); + glUniform1i(glGetUniformLocation(renderer->textShader, "uTex1"), 1); + glUniform1i(glGetUniformLocation(renderer->textShader, "uTex2"), 2); + glUniform1i(glGetUniformLocation(renderer->textShader, "uTex3"), 3); +} + +void Gles3_SetRenderTextFunction( + Gles3_Renderer *renderer, + void (*renderTextFunction)( + Clay_RenderCommand *cmd, Gles3_GlyphVtxArray *accum, void *userData), + void *userData) +{ + renderer->renderTextFunction = renderTextFunction; +} + +void Gles3_Render( + Gles3_Renderer *renderer, + Clay_RenderCommandArray cmds, + void *userData // eg. fonts +) +{ + Clay_Dimensions layoutDimensions = Clay_GetCurrentContext()->layoutDimensions; + renderer->screenWidth = layoutDimensions.width; + renderer->screenHeight = layoutDimensions.height; + + Gles3_QuadInstanceArray *quads = &renderer->quadInstanceArray; + Gles3_GlyphVtxArray *gVerts = &renderer->glyphVtxArray; + + gVerts->count = 0; + + for (int i = 0; i < cmds.length; i++) + { + Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&cmds, i); + Clay_BoundingBox boundingBox = (Clay_BoundingBox){ + .x = roundf(cmd->boundingBox.x), + .y = roundf(cmd->boundingBox.y), + .width = roundf(cmd->boundingBox.width), + .height = roundf(cmd->boundingBox.height), + }; + + bool scissorChanged = false; + switch (cmd->commandType) + { + case CLAY_RENDER_COMMAND_TYPE_TEXT: + { + renderer->renderTextFunction( + cmd, + &renderer->glyphVtxArray, + userData); + break; + } + case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: + case CLAY_RENDER_COMMAND_TYPE_IMAGE: + { + Clay_RectangleRenderData *config = &cmd->renderData.rectangle; + Clay_Color c = config->backgroundColor; + + // Convert to float 0..1 + float rf = c.r / 255.0f; + float gf = c.g / 255.0f; + float bf = c.b / 255.0f; + float af = c.a / 255.0f; + + bool isImage = cmd->commandType == CLAY_RENDER_COMMAND_TYPE_IMAGE; + + // Ensure we don't overflow the capacity + if (quads->count >= quads->capacity) + { + printf("Clay renderer: instance overflow!\n"); + break; + } + + int idx = quads->count; + RectInstance *dst = &quads->instData[idx]; + dst->x = boundingBox.x; + dst->y = boundingBox.y; + dst->w = boundingBox.width; + dst->h = boundingBox.height; + + if (isImage) + { + Gles3_ImageConfig *imgConf = (Gles3_ImageConfig *)cmd->renderData.image.imageData; + dst->u0 = imgConf->u0; + dst->v0 = imgConf->v0; + dst->u1 = imgConf->u1; + dst->v1 = imgConf->v1; + dst->texToUse = (float)imgConf->textureToUse; + } + else + { + dst->u0 = dst->v0 = 0.0f; + dst->u1 = dst->v1 = 1.0f; + dst->texToUse = -1.0f; // This means no image, use albedo color + } + + // colour + dst->r = rf; + dst->g = gf; + dst->b = bf; + dst->a = af; + + // corner radii + Clay_CornerRadius r = config->cornerRadius; + dst->radiusTL = r.topLeft; + dst->radiusTR = r.topRight; + dst->radiusBL = r.bottomLeft; + dst->radiusBR = r.bottomRight; + + dst->borderT = 0.0f; + dst->borderR = 0.0f; + dst->borderB = 0.0f; + dst->borderL = 0.0f; + + quads->count++; + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: + { + scissorChanged = true; + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: + { + scissorChanged = true; + break; + } + case CLAY_RENDER_COMMAND_TYPE_BORDER: + { + Clay_BorderRenderData *br = &cmd->renderData.border; + + float rf = br->color.r / 255.0f; + float gf = br->color.g / 255.0f; + float bf = br->color.b / 255.0f; + float af = br->color.a / 255.0f; + + float x = boundingBox.x; + float y = boundingBox.y; + float w = boundingBox.width; + float h = boundingBox.height; + + float top = br->width.top; + float bottom = br->width.bottom; + float left = br->width.left; + float right = br->width.right; + + int idx = quads->count; + RectInstance *dst = &quads->instData[idx]; + + dst->x = x - left; + dst->y = y - top; + dst->w = w + right; + dst->h = h + bottom; + + dst->borderB = bottom; + dst->borderL = left; + dst->borderT = top; + dst->borderR = right; + + // Clay borders are inset, but adding support to outset borders + // Is as easy as this + some minor changes in shader too + bool CLAY_BORDERS_ARE_INSET = true; + if (CLAY_BORDERS_ARE_INSET) + { + // Normal behaviour + dst->x = x; + dst->y = y; + dst->w = w; + dst->h = h; + } + else + { + // Hypotethical behaviour, if the borders were outside + dst->x = x - left; + dst->y = y - top; + dst->w = w + left + right; + dst->h = h + top + bottom; + } + + dst->u0 = 0.0f; + dst->v0 = 0.0f; + dst->u1 = 1.0f; + dst->v1 = 1.0f; + + dst->r = rf; + dst->g = gf; + dst->b = bf; + dst->a = af; + + dst->radiusTL = br->cornerRadius.topLeft; + dst->radiusTR = br->cornerRadius.topRight; + dst->radiusBR = br->cornerRadius.bottomRight; + dst->radiusBL = br->cornerRadius.bottomLeft; + + dst->texToUse = -1.0f; + + quads->count++; + break; + } + + case CLAY_RENDER_COMMAND_TYPE_CUSTOM: + { + // printf("Unhandled clay cmd: custom\n"); + break; + } + default: + { + printf("Error: unhandled render command\n"); + exit(1); + } + } + + // Flush draw calls if scissors about to change in this iteration + if (i == cmds.length - 1 || scissorChanged) + { + scissorChanged = false; + // Render Recatangles and Images + if (quads->count > 0) + { + glUseProgram(renderer->quadShaderId); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, renderer->imageTextures[0]); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, renderer->imageTextures[1]); + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, renderer->imageTextures[2]); + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, renderer->imageTextures[3]); + + // set uniforms + GLint locScreen = glGetUniformLocation(renderer->quadShaderId, "uScreen"); + glUniform2f(locScreen, + (float)renderer->screenWidth, + (float)renderer->screenHeight); + + glBindVertexArray(renderer->quadVAO); + + // upload all instances at once + glBindBuffer(GL_ARRAY_BUFFER, renderer->quadInstanceVBO); + + // rectangles are solid colour — disable atlas use + glBufferSubData(GL_ARRAY_BUFFER, + 0, + quads->count * sizeof(RectInstance), + quads->instData); + + // draw unit quad (4 verts) instanced + glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, quads->count); + renderer->totalDrawCallsToOpenGl += 1; + + glBindVertexArray(0); + glUseProgram(0); + } + // Clrear instance arrays, as they were flushed to their render calls + quads->count = 0; + + // Text rendering + if (renderer->glyphVtxArray.count > 0) + { + glUseProgram(renderer->textShader); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, renderer->fontTextures[0]); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, renderer->fontTextures[1]); + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, renderer->fontTextures[2]); + + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, renderer->fontTextures[3]); + + GLint uScreenLoc = glGetUniformLocation(renderer->textShader, "uScreen"); + glUniform2f(uScreenLoc, renderer->screenWidth, renderer->screenHeight); + + glBindVertexArray(renderer->textVAO); + glBindBuffer(GL_ARRAY_BUFFER, renderer->textVBO); + + glBufferSubData( + GL_ARRAY_BUFFER, + 0, + sizeof(struct GlyphVtx) * 6 * gVerts->count, + renderer->glyphVtxArray.instData); + + glDrawArrays(GL_TRIANGLES, 0, renderer->glyphVtxArray.count * 6); + renderer->totalDrawCallsToOpenGl += 1; + + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + } + renderer->glyphVtxArray.count = 0; + + if (cmd->commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_START) + { + Clay_BoundingBox bb = cmd->boundingBox; + GLint x = (GLint)bb.x; + GLint y = (GLint)(renderer->screenHeight - (bb.y + bb.height)); + GLsizei w = (GLsizei)bb.width; + GLsizei h = (GLsizei)bb.height; + + glEnable(GL_SCISSOR_TEST); + glScissor(x, y, w, h); + } + else + { + glDisable(GL_SCISSOR_TEST); + } + } + } +} +#endif +#endif \ No newline at end of file diff --git a/renderers/GLES3/clay_renderer_gles3_loader_stb.c b/renderers/GLES3/clay_renderer_gles3_loader_stb.c new file mode 100644 index 0000000..b98da72 --- /dev/null +++ b/renderers/GLES3/clay_renderer_gles3_loader_stb.c @@ -0,0 +1,368 @@ +#pragma once + +#include +#include + +#include +#include + +#include "clay_renderer_gles3.h" + +typedef struct LoadedImage +{ + unsigned char *data; + int width; + int height; + int channels; +} LoadedImage; + +typedef struct LoadedImageInternal +{ + LoadedImage pub; +} LoadedImageInternal; + +static LoadedImageInternal g_imageSlot; + +const LoadedImage *loadImage(const char *path, bool flip) +{ + if (!path) + return NULL; + + stbi_set_flip_vertically_on_load(flip ? 1 : 0); + + int w = 0; + int h = 0; + int c = 0; + + unsigned char *data = stbi_load(path, &w, &h, &c, 0); + if (!data) + { + // Failed + g_imageSlot.pub.data = NULL; + g_imageSlot.pub.width = 0; + g_imageSlot.pub.height = 0; + g_imageSlot.pub.channels = 0; + return NULL; + } + + g_imageSlot.pub.data = data; + g_imageSlot.pub.width = w; + g_imageSlot.pub.height = h; + g_imageSlot.pub.channels = c; + + return &g_imageSlot.pub; +} + +void freeImage(const LoadedImage *img) +{ + if (!img || !img->data) + return; + + // cast back to internal container + stbi_image_free((void *)img->data); + + // reset slot + g_imageSlot.pub.data = NULL; + g_imageSlot.pub.width = 0; + g_imageSlot.pub.height = 0; + g_imageSlot.pub.channels = 0; +} + +typedef struct Stb_FontData +{ + float bakePxH; // font baking height (e.g. 48.0f) + float ascentPx; // in baked pixels (at bake_px size) + float descentPx; // usually negative (at bake_px size) + int firstChar; // e.g. 32 + int charCount; // e.g. 96 + stbtt_bakedchar *cdata; + int atlasW; + int atlasH; +} Stb_FontData; + +bool Stb_LoadFont( + GLuint *textureOut, + Stb_FontData *fontOut, + const char *ttfPath, + float bakePxH, // Height of a char in pixels + int atlasW, // Width of atlas in pixels + int atlasH // Height of atlas in pixels +) +{ + fontOut->firstChar = 32; // ASCII space + fontOut->charCount = 96; // 32..127 + fontOut->bakePxH = bakePxH; + fontOut->atlasW = atlasW; + fontOut->atlasH = atlasH; + + // allocate baked-char array + fontOut->cdata = (stbtt_bakedchar *)malloc( + sizeof(stbtt_bakedchar) // Store baked info + * fontOut->charCount // For each char + ); + if (!fontOut->cdata) + { + fprintf(stderr, "Cannot allocate cdata\n"); + return false; + } + + FILE *f = fopen(ttfPath, "rb"); + if (!f) + { + fprintf(stderr, "Could not open font: %s\n", ttfPath); + return false; + } + + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + + unsigned char *ttf_buf = (unsigned char *)malloc(sz); + fread(ttf_buf, 1, sz, f); + fclose(f); + + // temporary atlas memory + unsigned char *atlas = (unsigned char *)malloc(atlasW * atlasH); + memset(atlas, 0, atlasW * atlasH); + + // bake + int res = stbtt_BakeFontBitmap( + ttf_buf, // raw TTF file + 0, // font index inside TTF (0 = first font) + bakePxH, // pixel height of glyphs to generate + atlas, // OUT: bitmap buffer (unsigned char*) + atlasW, atlasH, // size of bitmap buffer + fontOut->firstChar, // first character to bake (e.g., 32 = space) + fontOut->charCount, // how many sequential chars to bake + fontOut->cdata // OUT: array of stbtt_bakedchar + ); + + stbtt_fontinfo fi; + if (!stbtt_InitFont(&fi, ttf_buf, stbtt_GetFontOffsetForIndex(ttf_buf, 0))) + { + return false; + } + + int ascent, descent, lineGap; + stbtt_GetFontVMetrics(&fi, &ascent, &descent, &lineGap); + + // Convert the font's "font units" to pixels proportional to bakePxH size: + float scaleForBake = stbtt_ScaleForPixelHeight(&fi, bakePxH); + + fontOut->ascentPx = ascent * scaleForBake; + fontOut->descentPx = descent * scaleForBake; // this is typically negative + + free(ttf_buf); + + if (res <= 0) + { + fprintf(stderr, "Font baking failed\n"); + free(atlas); + free(fontOut->cdata); + fontOut->cdata = NULL; + return false; + } + + // Creating glyphVtxArray atlas texture + glGenTextures(1, textureOut); + glBindTexture(GL_TEXTURE_2D, *textureOut); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, + atlasW, atlasH, + 0, GL_RED, GL_UNSIGNED_BYTE, atlas); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glBindTexture(GL_TEXTURE_2D, 0); + + free(atlas); + + return true; +} + +static inline Clay_Dimensions Stb_MeasureText( + Clay_StringSlice glyphVtxArray, + Clay_TextElementConfig *config, + void *userData) +{ + Stb_FontData *fontData = (Stb_FontData *)userData; + + if (!fontData->cdata) + { + fprintf( + stderr, + "MeasureText cannot do anything when cdata is not baked: '%.*s' → %d x %d px\n", + (int)glyphVtxArray.length, glyphVtxArray.chars, 0, 0); + return (Clay_Dimensions){.width = 0, .height = 0}; + } + + float x = 0.0f; + float y = 0.0f; + + const char *str = glyphVtxArray.chars; + int len = glyphVtxArray.length; + + float scale = config->fontSize / fontData->bakePxH; + + float letterSpacing = (float)config->letterSpacing; + float lineHeight = (config->lineHeight > 0) + ? (float)config->lineHeight + : fontData->bakePxH; + + for (int i = 0; i < len; i++) + { + unsigned char c = str[i]; + + if (c < fontData->firstChar // before range + || c >= fontData->firstChar + fontData->charCount // after range + ) + { + // Unsupported char: treat as space + fprintf(stderr, "illegal char %d\n", (int)c); + x += fontData->bakePxH * 0.25f; + continue; + } + + stbtt_bakedchar *b = &fontData->cdata[c - fontData->firstChar]; + + // horizontal advance while moving along word characters + x += b->xadvance * scale + letterSpacing; + } + + float ascent = fontData->ascentPx * scale; + float descent = fontData->descentPx * scale; // negative + float lineH = (ascent - descent); // total line height in pixels (at requested fontSize) + + return (Clay_Dimensions){ + .width = x, + .height = y + lineH, + }; +} + +static inline void Stb_RenderText( + Clay_RenderCommand *cmd, + Gles3_GlyphVtxArray *glyphVtxArray, + void *userData) +{ + const Clay_TextRenderData *tr = &cmd->renderData.text; + + float cr = tr->textColor.r / 255.0f; + float cg = tr->textColor.g / 255.0f; + float cb = tr->textColor.b / 255.0f; + float ca = tr->textColor.a / 255.0f; + float fontToUse = (float)tr->fontId; + + Stb_FontData *fontArray = (Stb_FontData *)userData; + Stb_FontData *stbFontData = &fontArray[tr->fontId]; + if (!stbFontData->cdata) + return; + + Clay_StringSlice ss = tr->stringContents; + const char *txt = ss.chars; + int len = (int)ss.length; + + float scale = tr->fontSize / stbFontData->bakePxH; + float ascent = stbFontData->ascentPx * scale; // pixels above baseline + float x = cmd->boundingBox.x; + float y = cmd->boundingBox.y + ascent; // baseline (note: no descent) + + for (int i = 0; i < len; i++) + { + char ch = txt[i]; + + int idx = ch - stbFontData->firstChar; + if (idx < 0 || idx >= stbFontData->charCount) + { + continue; + } + + stbtt_bakedchar *bc = &stbFontData->cdata[idx]; + + float gw = (float)(bc->x1 - bc->x0); // glyph width in atlas pixels + float gh = (float)(bc->y1 - bc->y0); // glyph height + + float sw = gw * scale; // scaled width on screen + float sh = gh * scale; // scaled height + + float ox = bc->xoff * scale; // baseline offset + float oy = bc->yoff * scale; + + // top-left corner on screen (pixel coords) + float x0 = x + ox; + float y0 = y + oy; + float x1 = x0 + sw; + float y1 = y0 + sh; + + // atlas size (you can make it configurable later) + float atlasW = stbFontData->atlasW; + float atlasH = stbFontData->atlasH; + + float u0 = bc->x0 / atlasW; + float v0 = bc->y0 / atlasH; + float u1 = bc->x1 / atlasW; + float v1 = bc->y1 / atlasH; + + // append 6 vertices (two triangles) to your buffer + GlyphVtx *v = &glyphVtxArray->instData[glyphVtxArray->count * 6]; + + v[0] = (GlyphVtx){x0, y0, u0, v0, cr, cg, cb, ca, fontToUse}; + v[1] = (GlyphVtx){x1, y0, u1, v0, cr, cg, cb, ca, fontToUse}; + v[2] = (GlyphVtx){x0, y1, u0, v1, cr, cg, cb, ca, fontToUse}; + + v[3] = (GlyphVtx){x0, y1, u0, v1, cr, cg, cb, ca, fontToUse}; + v[4] = (GlyphVtx){x1, y0, u1, v0, cr, cg, cb, ca, fontToUse}; + v[5] = (GlyphVtx){x1, y1, u1, v1, cr, cg, cb, ca, fontToUse}; + + // advance pen by baked xadvance + letter spacing + x += (bc->xadvance * scale) + tr->letterSpacing; + + // prevent buffer overrun + if (glyphVtxArray->count >= glyphVtxArray->capacity) + { + break; + } + glyphVtxArray->count++; + } +} + +bool Stb_LoadImage(GLuint *textureOut, const char *path) +{ + const LoadedImage *li = loadImage(path, false); + if (!li || !li->data) + { + fprintf(stderr, "Failed to load texture at: %s\n", path); + return false; + } + + glGenTextures(1, textureOut); + glBindTexture(GL_TEXTURE_2D, *textureOut); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + GLenum format = (li->channels == 4) ? GL_RGBA : GL_RGB; + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + glTexImage2D( + GL_TEXTURE_2D, // target + 0, // level + format, // internal format int + li->width, + li->height, + 0, // border + format, // format, GLEnum + GL_UNSIGNED_BYTE, // Type + li->data // pixels + ); + + glGenerateMipmap(GL_TEXTURE_2D); + + freeImage(li); + return true; +} \ No newline at end of file