diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ab1b94..7a9543e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ option(CLAY_INCLUDE_SDL3_EXAMPLES "Build SDL 3 examples" OFF) option(CLAY_INCLUDE_WIN32_GDI_EXAMPLES "Build Win32 GDI examples" OFF) option(CLAY_INCLUDE_SOKOL_EXAMPLES "Build Sokol examples" OFF) option(CLAY_INCLUDE_PLAYDATE_EXAMPLES "Build Playdate examples" OFF) +option(CLAY_INCLUDE_TIGR_EXAMPLES "Build Tigr examples" OFF) message(STATUS "CLAY_INCLUDE_DEMOS: ${CLAY_INCLUDE_DEMOS}") @@ -45,6 +46,10 @@ if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_SOKOL_EXAMPLES) add_subdirectory("examples/sokol-video-demo") add_subdirectory("examples/sokol-corner-radius") endif() +if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_TIGR_EXAMPLES) + add_subdirectory("examples/tigr-demo") +endif() + # Playdate example not included in ALL because users need to install the playdate SDK first which requires a license agreement if(CLAY_INCLUDE_PLAYDATE_EXAMPLES) diff --git a/examples/tigr-demo/CMakeLists.txt b/examples/tigr-demo/CMakeLists.txt new file mode 100644 index 0000000..f159169 --- /dev/null +++ b/examples/tigr-demo/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.16) + +project(demo C) + +set(CMAKE_C_STANDARD 99) + +# ----------------------------- +# EDIT ME WITH RELATIVE PATH TO: +# tigr.c & tigr.h +set(TIGR_DIR "../../../tigr") + +add_executable(demo + main.c + ${TIGR_DIR}/tigr.c + ../../renderers/tigr/tigr_utils.c + ../../renderers/tigr/clay_renderer_tigr.c +) + +target_include_directories(demo PRIVATE + ${TIGR_DIR} +) + +# Platform-specific libraries +if(WIN32) + target_link_libraries(demo PRIVATE + opengl32 + gdi32 + ) + +elseif(APPLE) + find_library(OPENGL_FRAMEWORK OpenGL) + find_library(COCOA_FRAMEWORK Cocoa) + + target_link_libraries(demo PRIVATE + ${OPENGL_FRAMEWORK} + ${COCOA_FRAMEWORK} + ) + +elseif(UNIX) + target_link_libraries(demo PRIVATE + m + GLU + GL + X11 + ) +endif() + +add_custom_command(TARGET demo POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/resources + $/resources +) diff --git a/examples/tigr-demo/main.c b/examples/tigr-demo/main.c new file mode 100644 index 0000000..cc4cfba --- /dev/null +++ b/examples/tigr-demo/main.c @@ -0,0 +1,121 @@ +/** + * Simple example based off the Clay README + */ +#define CLAY_IMPLEMENTATION +#include "../../clay.h" + +#include "../../renderers/tigr/clay_renderer_tigr.h" + +const Clay_Color COLOR_LIGHT = (Clay_Color) {224, 215, 210, 255}; +const Clay_Color COLOR_RED = (Clay_Color) {168, 66, 28, 255}; +const Clay_Color COLOR_ORANGE = (Clay_Color) {225, 138, 50, 255}; + +void HandleClayErrors(Clay_ErrorData errorData) { + // See the Clay_ErrorData struct for more information + printf("%s\n", errorData.errorText.chars); + switch(errorData.errorType) { + // etc + } +} + +// Layout config is just a struct that can be declared statically, or inline +Clay_ElementDeclaration sidebarItemConfig = (Clay_ElementDeclaration) { + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(50) } + }, + .backgroundColor = COLOR_ORANGE +}; + +// Re-useable components are just normal functions +void SidebarItemComponent(char* my_str, int number) { + snprintf(my_str, 8, "Box #%02d", number); + Clay_String box_txt = (Clay_String){.isStaticallyAllocated = false, .length = 8, .chars = my_str}; + CLAY_AUTO_ID(sidebarItemConfig) { + CLAY_TEXT(box_txt, { .fontSize = 8, .textColor = {255, 255, 255, 255} }); + } +} + +int main() { + uint64_t totalMemorySize = Clay_MinMemorySize(); + Clay_Arena arena = Clay_CreateArenaWithCapacityAndMemory(totalMemorySize, malloc(totalMemorySize)); + + int screenWidth = 1000; + int screenHeight = 600; + + int mousePositionX, mousePositionY, isMouseDown; + float mouseWheelX, mouseWheelY; + Clay_Initialize(arena, (Clay_Dimensions) { screenWidth, screenHeight }, (Clay_ErrorHandler) { HandleClayErrors }); + Clay_SetMeasureTextFunction(Clay_Tigr_MeasureText, NULL); + + Tigr* win = tigrWindow(screenWidth, screenHeight, "Clay & Tigr", TIGR_AUTO); + + /* 20 8 byte strings */ + char* my_str = calloc(20, 8); + if(my_str == NULL) { + printf("Failed to allocate memoery\n"); + return -1; + } + + uint64_t frame_cnt = 0; + while(!tigrClosed(win) && !tigrKeyDown(win, TK_ESCAPE)) { + + int deltaTime = tigrTime(); // tigrTime return the time since it was last called + tigrMouse(win, &mousePositionX, &mousePositionY, &isMouseDown); + tigrScrollWheel(win, &mouseWheelX, &mouseWheelY); + + screenWidth = win->w; + screenHeight = win->h; + Clay_SetLayoutDimensions((Clay_Dimensions) { screenWidth, screenHeight }); + Clay_SetPointerState((Clay_Vector2) { mousePositionX, mousePositionY }, isMouseDown); + Clay_UpdateScrollContainers(true, (Clay_Vector2) { mouseWheelX, mouseWheelY }, deltaTime); + + Clay_BeginLayout(); + + CLAY(CLAY_ID("OuterContainer"), { + .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)}, + .padding = CLAY_PADDING_ALL(16), .childGap = 16 }, + .backgroundColor = {41, 41, 61,255} }) { + + CLAY(CLAY_ID("SideBarContainer"), { + .layout = { .layoutDirection = CLAY_TOP_TO_BOTTOM, + .sizing = {CLAY_SIZING_FIXED(300), CLAY_SIZING_GROW(0)}, + .padding = CLAY_PADDING_ALL(16), + .childGap = 16}, + .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() }, + .backgroundColor = COLOR_LIGHT }) { + + CLAY(CLAY_ID("ProfilePictureOuter"), { + .layout = { .sizing = { .width = CLAY_SIZING_GROW(0) }, + .padding = CLAY_PADDING_ALL(16), + .childGap = 16, + .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } }, + .backgroundColor = COLOR_RED }) { + + CLAY(CLAY_ID("ProfilePicture"), { + .layout = { .sizing = { .width = CLAY_SIZING_FIXED(70), .height = CLAY_SIZING_FIXED(86) }}, + .image = { .imageData = "./resources/leroy.png" } }) {} + + + CLAY_TEXT(CLAY_STRING("Clay & Tigr - UI Library"), { .fontSize = 16, .textColor = {255, 255, 255, 255} }); + } + + for (int i = 0; i < 20; i++) { + SidebarItemComponent(my_str + (8 * i), i); + } + + } + + CLAY(CLAY_ID("MainContent"), { + .layout = { .sizing = { .width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0) } }, + .backgroundColor = COLOR_LIGHT }) {} + } + + Clay_RenderCommandArray renderCommands = Clay_EndLayout(deltaTime); + Clay_Tigr_Render(renderCommands, win); + + tigrUpdate(win); + } + + free(my_str); + tigrFree(win); +} diff --git a/examples/tigr-demo/resources/leroy.png b/examples/tigr-demo/resources/leroy.png new file mode 100755 index 0000000..f4d163a Binary files /dev/null and b/examples/tigr-demo/resources/leroy.png differ diff --git a/renderers/tigr/clay_renderer_tigr.c b/renderers/tigr/clay_renderer_tigr.c new file mode 100644 index 0000000..80928a5 --- /dev/null +++ b/renderers/tigr/clay_renderer_tigr.c @@ -0,0 +1,106 @@ +#include "clay_renderer_tigr.h" + +#define CLAY_TO_TPIXEL(color) (TPixel){color.r, color.g, color.b, color.a} + +void Clay_Tigr_Render(Clay_RenderCommandArray commands, Tigr* ctx) { + for(size_t i = 0; i < commands.length; i++) { + Clay_RenderCommand *command = Clay_RenderCommandArray_Get(&commands, i); + + switch(command->commandType) { + case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { + Clay_RectangleRenderData* config = &command->renderData.rectangle; + Clay_BoundingBox bb = command->boundingBox; + + int box_radius = config->cornerRadius.topLeft; // only take take top left radius + + tigrFillArcRect(ctx, bb.x, bb.y, bb.width, bb.height, box_radius, CLAY_TO_TPIXEL(config->backgroundColor)); + + break; + } + case CLAY_RENDER_COMMAND_TYPE_TEXT: { + Clay_TextRenderData* config = &command->renderData.text; + Clay_String toTerminate = (Clay_String){ .chars = config->stringContents.chars, .length = config->stringContents.length, .isStaticallyAllocated = false }; + char* text = Clay_to_Cstr(&toTerminate); + + Clay_BoundingBox bb = command->boundingBox; + Clay_Color color = config->textColor; + + float scale = config->fontSize / 8.0f; // tfont is roughly 8px high + + Tigr* temp = tigrBitmap( + tigrTextWidth(tfont, text), + tfont->glyphs->h + ); + + tigrClear(temp, tigrRGBA(0,0,0,0)); + tigrPrint(temp, tfont, 0, 0, CLAY_TO_TPIXEL(color), text); + + tigrBlitScale(ctx, temp, bb.x, bb.y, 0, 0, temp->w, temp->h, temp->w * scale, temp->h * scale); + + tigrFree(temp); + free(text); + break; + } + case CLAY_RENDER_COMMAND_TYPE_BORDER: { + Clay_BorderRenderData* config = &command->renderData.border; + Clay_BoundingBox bb = command->boundingBox; + + int box_radius = config->cornerRadius.topLeft; + + tigrArcRect(ctx, bb.x, bb.y, bb.width, bb.height, box_radius, CLAY_TO_TPIXEL(config->color)); + + break; + } + case CLAY_RENDER_COMMAND_TYPE_IMAGE: { + Clay_ImageRenderData *config = &command->renderData.image; + Clay_BoundingBox bb = command->boundingBox; + + char* path = config->imageData; + + Tigr* img = tigrLoadImage(path); + + double image_w = img->w; + double image_h = img->h; + + /* Calculate the scaling factor to fit within the bounding box while preserving aspect ratio */ + double scale_w = bb.width / image_w; + double scale_h = bb.height / image_h; + double scale = (scale_w < scale_h) ? scale_w : scale_h; // Use the smaller scaling factor + + /* Apply the same scale to both dimensions to preserve aspect ratio */ + double scale_x = scale; + double scale_y = scale; + + /* Calculate the scaled image dimensions */ + double scaled_w = image_w * scale_x; + double scaled_h = image_h * scale_y; + + /* Adjust the x and y coordinates to center the scaled image within the bounding box */ + double centered_x = bb.x + (bb.width - scaled_w) / 2.0; + double centered_y = bb.y + (bb.height - scaled_h) / 2.0; + + /* Blit the scaled and centered image */ + tigrBlit(ctx, img, centered_x, centered_y, 0, 0, scaled_w, scaled_h); + + /* Clean up the source surface */ + tigrFree(img); + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { + Clay_BoundingBox bb = command->boundingBox; + tigrClip(ctx, bb.x, bb.y, bb.width, bb.height); + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: { + tigrClip(ctx, 0, 0, -1, -1); + break; + } + case CLAY_RENDER_COMMAND_TYPE_CUSTOM: { + // Slot your custom elements in here. + } + default: { + fprintf(stderr, "Unknown command type %d\n", (int) command->commandType); + } + } + } +} diff --git a/renderers/tigr/clay_renderer_tigr.h b/renderers/tigr/clay_renderer_tigr.h new file mode 100644 index 0000000..cbc56c0 --- /dev/null +++ b/renderers/tigr/clay_renderer_tigr.h @@ -0,0 +1,75 @@ +// Copyright (c) 2026 Noah Arthur Breedy +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the +// use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software in a +// product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not +// be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source +// distribution. +// +// SPDX-License-Identifier: Zlib + +#ifndef __CLAY_TIGR_RENDERER_H__ +#define __CLAY_TIGR_RENDERER_H__ + +#include +#include +#include +#include + +#include "../../clay.h" +#include "tigr.h" +#include "tigr_utils.h" // for our rounded borders + + +#define CLAY_TO_TPIXEL(color) (TPixel){color.r, color.g, color.b, color.a} + +/* Render the command queue to the `Tigr*` instance provided */ +void Clay_Tigr_Render(Clay_RenderCommandArray commands, Tigr* ctx); + +/* Return a null-terminated copy of Clay_String `str` + * caller is required to free + * */ +static inline char *Clay_to_Cstr(Clay_String *str) { + char* copy = (char*) malloc(str->length + 1); + if (!copy) { + fprintf(stderr, "Memory allocation failed\n"); + return NULL; + } + memcpy(copy, str->chars, str->length); + copy[str->length] = '\0'; + return copy; +} + +/* Measure text using built-in Tigr functions */ +static inline Clay_Dimensions Clay_Tigr_MeasureText(Clay_StringSlice str, Clay_TextElementConfig *config, void *userData) { + + Clay_String toTerminate = (Clay_String){ .chars = str.chars, .length = str.length, .isStaticallyAllocated = false }; + + char* cstr = Clay_to_Cstr(&toTerminate); + + int text_width = tigrTextWidth(tfont, cstr) * (config->fontSize/8); + int text_height = tigrTextHeight(tfont, cstr) * (config->fontSize/8); + + free(cstr); + + // Return dimensions + return (Clay_Dimensions){ + .width = (float)text_width, + .height = (float)text_height + }; +} + +#endif /* __CLAY_TIGR_RENDERER_H__ */ diff --git a/renderers/tigr/tigr_utils.c b/renderers/tigr/tigr_utils.c new file mode 100644 index 0000000..31b4a64 --- /dev/null +++ b/renderers/tigr/tigr_utils.c @@ -0,0 +1,208 @@ +#include "tigr_utils.h" + +enum ARC_STATES { + NOT_DRAWN, + STARTS_HERE, + ALL_DRAWN, + ENDS_HERE, + STARTS_ENDS_HERE +}; + +/* Radian to degree conversion value */ +#define R_TO_D 57.29578 + +/* global memory for our arc renderer */ +int arc_sector[9]; + +void PositiveSectorPoint(Tigr* ctx, int x, int y, int s, int sp, int ep, TPixel col) { + + if (arc_sector[s] == NOT_DRAWN) return; + + /* Draw all points of this sector */ + if (arc_sector[s] == ALL_DRAWN) { + tigrPlot(ctx, x, y, col); + return; + } + + /* draw all points flowing to right */ + if (arc_sector[s] == STARTS_HERE) { + if (x >=sp) { + tigrPlot(ctx, x, y, col); + return; + } + } + + /* draw all points flowing from left */ + if (arc_sector[s] == ENDS_HERE) { + if (x <= ep) { + tigrPlot(ctx, x, y, col); + return; + } + } + + /* fill only sections of this sector if ((x >= sp) && (x <= ep)) pixel (x, y); */ + if (arc_sector[s] == STARTS_ENDS_HERE) { + } +} + +void NegativeSectorPoint(Tigr* ctx, int x, int y, int s, int sp, int ep, TPixel col) { + + if (arc_sector[s] == NOT_DRAWN) return; + + /* Draw all points in this sector */ + if (arc_sector[s] == ALL_DRAWN) { + tigrPlot(ctx, x, y, col); + return; + } + + /* Draw all points flowing to the left */ + if (arc_sector[s] == STARTS_HERE) { + if (x <= sp) { + tigrPlot(ctx, x, y, col); + return; + } + } + + /* Draw all points flowing from the right if (x >= ep) {plot(x, y); return; } */ + if (arc_sector[s] == ENDS_HERE) { + } + + /* fill only sections of this sector */ + if (arc_sector[s] == STARTS_ENDS_HERE) { + if ((x >= ep) && (x <= sp)) { + tigrPlot(ctx, x, y, col); + } + } +} + +void tigrArc(Tigr* ctx, int xc, int yc, int sa, int ea, int r, TPixel col) { + + int start_sector, end_sector; + + int i; + int x, y; + int ep, sp, d; + + /* Clear all the arc sector flags */ + for(i = 0; i < 8; i++) { + arc_sector[i] = NOT_DRAWN; + } + + /* Calculate start and end arc sectors */ + start_sector = sa / 45; + end_sector = ea / 45; + + if(start_sector == end_sector) { + arc_sector[start_sector] = STARTS_ENDS_HERE; + }else { + /* Set all of the possible drawn sector flags */ + for(i = start_sector; i < end_sector; i++) { + arc_sector[i] = ALL_DRAWN; + } + arc_sector[start_sector] = STARTS_HERE; + arc_sector[end_sector] = ENDS_HERE; + } + + /* Calculate the Start and End points */ + x = 0; + y = r; + + sp = ((double)xc + (double)r * cos((double)sa/R_TO_D)); + ep = ((double)xc + (double)r * cos((double)ea/R_TO_D)); + d = 2 * (1 - r); + + while(y > x) { + NegativeSectorPoint(ctx, xc + y, yc + x, 0, sp, ep, col); + NegativeSectorPoint(ctx, xc + x, yc + y, 1, sp, ep, col); + NegativeSectorPoint(ctx, xc - x, yc + y, 2, sp, ep, col); + NegativeSectorPoint(ctx, xc - y, yc + x, 3, sp, ep, col); + PositiveSectorPoint(ctx, xc - y, yc - x, 4, sp, ep, col); + PositiveSectorPoint(ctx, xc - x, yc - y, 5, sp, ep, col); + PositiveSectorPoint(ctx, xc + x, yc - y, 6, sp, ep, col); + PositiveSectorPoint(ctx, xc + y, yc - x, 7, sp, ep, col); + if (d + y > 0) { + y = y - 1; + d = d - 2 * y + 1; + } + else { + if (x > d) { + x = x + 1; + d = d + 2 * x + 1; + } + } + } + +} + +void tigrArcRect(Tigr* ctx, int x, int y, int w, int h, int r, TPixel col) { + + tigrArc(ctx, x + r, y, 180, 270, r, col); // top left + tigrArc(ctx, x - r + w, y, 270, 360, r, col); // top right + tigrArc(ctx, x - r + w, y + h, 0, 90, r, col); // bottom right + tigrArc(ctx, x + r, y + h, 90, 180, r, col); // bottom left + + /* These are the connecting in lines */ + tigrLine(ctx, x + r, y - r, x + w - r, y - r, col); // top + tigrLine(ctx, x + r, y + h + r, x + w - r, y + h + r, col); // bottom + tigrLine(ctx, x, y, x, y + h, col); // left + tigrLine(ctx, x + w, y, x + w, y + h, col); // right + +} + +/* The reason I dont call the tigrArcRect function is to save on draw calls */ +void tigrFillArcRect(Tigr* ctx, int x, int y, int w, int h, int r, TPixel col) { + + /* Build the base frame */ + tigrFillRect(ctx, x - 1, y, w + 3, h, col); + tigrFillRect(ctx, x + r, y - r - 1, w - (r * 2) , h + (r * 2) + 3, col); + + /* Draw the corners */ + tigrArc(ctx, x + r, y, 180, 270, r, col); // top left + tigrArc(ctx, x - r + w, y, 270, 360, r, col); // top right + tigrArc(ctx, x - r + w, y + h, 0, 90, r, col); // bottom right + tigrArc(ctx, x + r, y + h, 90, 180, r, col); // bottom left + + /* Fill in the gaps */ + tigrFillCircle(ctx, x + r, y, r, col); // top left + tigrFillCircle(ctx, x + w - r, y, r, col); // top right + tigrFillCircle(ctx, x + w - r, y + h, r, col); // bottom right + tigrFillCircle(ctx, x + r, y + h, r, col); // bottom left + +} + +/* ChatGPT generated blitScale function */ +void tigrBlitScale(Tigr* dst, Tigr* src, int dx, int dy, int sx, int sy, int sw, int sh, int dw, int dh) { + for(int y = 0; y < dh; y++) + { + int srcY = sy + (y * sh) / dh; + + if(srcY < 0 || srcY >= src->h) + continue; + + int dstY = dy + y; + + if(dstY < 0 || dstY >= dst->h) + continue; + + for(int x = 0; x < dw; x++) + { + int srcX = sx + (x * sw) / dw; + + if(srcX < 0 || srcX >= src->w) + continue; + + int dstX = dx + x; + + if(dstX < 0 || dstX >= dst->w) + continue; + + TPixel p = src->pix[srcY * src->w + srcX]; + + /* Skip transparent pixels */ + if(p.a == 0) + continue; + + tigrPlot(dst, dstX, dstY, p); + } + } +} diff --git a/renderers/tigr/tigr_utils.h b/renderers/tigr/tigr_utils.h new file mode 100644 index 0000000..d1bb8c0 --- /dev/null +++ b/renderers/tigr/tigr_utils.h @@ -0,0 +1,31 @@ +#ifndef __TIGR_UTILS_H__ +#define __TIGR_UTILS_H__ + +#include + +#include "tigr.h" + +/* Fast arc drawing algorithm based on Bressenham Circle Routine + * + * taken from --> https://www.scattergood.io/arc-drawing-algorithm/ + */ +void tigrArc(Tigr* ctx, int xc, int yc, int sa, int ea, int r, TPixel col); + +/* Function to draw rectangles with arches in the angles of them + * makes use of the tigrArc function + * + * This is the wire frame one + */ +void tigrArcRect(Tigr* ctx, int x, int y, int w, int h, int r, TPixel col); + +/* Function to draw rectangles with arches in the angles of them + * makes use of the tigrArc function + * + * This is the filled one + */ +void tigrFillArcRect(Tigr* ctx, int x, int y, int w, int h, int r, TPixel col); + +/* ChatGPT generated blitScale function */ +void tigrBlitScale(Tigr* dst, Tigr* src, int dx, int dy, int sx, int sy, int sw, int sh, int dw, int dh); + +#endif /* __TIGR_UTILS_H__ */