From bed2d5b328c2fa5c19d7e8760a0cb7ce3c4e9dc5 Mon Sep 17 00:00:00 2001 From: Matthew Jennings Date: Mon, 5 May 2025 18:05:53 +0300 Subject: [PATCH] basics working on the playdate --- CMakeLists.txt | 4 + examples/playdate-project-example/.gitignore | 2 + .../playdate-project-example/CmakeLists.txt | 40 +++++ .../playdate-project-example/Source/pdxinfo | 5 + examples/playdate-project-example/main.c | 103 ++++++++++++ renderers/playdate/clay_renderer_playdate.c | 156 ++++++++++++++++++ 6 files changed, 310 insertions(+) create mode 100644 examples/playdate-project-example/.gitignore create mode 100644 examples/playdate-project-example/CmakeLists.txt create mode 100644 examples/playdate-project-example/Source/pdxinfo create mode 100644 examples/playdate-project-example/main.c create mode 100644 renderers/playdate/clay_renderer_playdate.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ddea29..3bb05c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ option(CLAY_INCLUDE_SDL2_EXAMPLES "Build SDL 2 examples" OFF) 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) message(STATUS "CLAY_INCLUDE_DEMOS: ${CLAY_INCLUDE_DEMOS}") @@ -42,6 +43,9 @@ 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_PLAYDATE_EXAMPLES) + add_subdirectory("examples/playdate-project-example") +endif() if(WIN32) # Build only for Win or Wine if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_WIN32_GDI_EXAMPLES) diff --git a/examples/playdate-project-example/.gitignore b/examples/playdate-project-example/.gitignore new file mode 100644 index 0000000..fd2fd96 --- /dev/null +++ b/examples/playdate-project-example/.gitignore @@ -0,0 +1,2 @@ +clay_playdate_example.pdx +Source/pdex.dylib diff --git a/examples/playdate-project-example/CmakeLists.txt b/examples/playdate-project-example/CmakeLists.txt new file mode 100644 index 0000000..c4aaf35 --- /dev/null +++ b/examples/playdate-project-example/CmakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.27) +set(CMAKE_C_STANDARD 99) + +set(ENVSDK $ENV{PLAYDATE_SDK_PATH}) + +if (NOT ${ENVSDK} STREQUAL "") + # Convert path from Windows + file(TO_CMAKE_PATH ${ENVSDK} SDK) +else() + execute_process( + COMMAND bash -c "egrep '^\\s*SDKRoot' $HOME/.Playdate/config" + COMMAND head -n 1 + COMMAND cut -c9- + OUTPUT_VARIABLE SDK + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +endif() + +if (NOT EXISTS ${SDK}) + message(FATAL_ERROR "SDK Path not found; set ENV value PLAYDATE_SDK_PATH") + return() +endif() + +set(CMAKE_CONFIGURATION_TYPES "Debug;Release") +set(CMAKE_XCODE_GENERATE_SCHEME TRUE) + +# Game Name Customization +set(PLAYDATE_GAME_NAME clay_playdate_example) +set(PLAYDATE_GAME_DEVICE clay_playdate_example_DEVICE) + +project(${PLAYDATE_GAME_NAME} C ASM) + +if (TOOLCHAIN STREQUAL "armgcc") + add_executable(${PLAYDATE_GAME_DEVICE} main.c) +else() + add_library(${PLAYDATE_GAME_NAME} SHARED main.c) +endif() + +include(${SDK}/C_API/buildsupport/playdate_game.cmake) + diff --git a/examples/playdate-project-example/Source/pdxinfo b/examples/playdate-project-example/Source/pdxinfo new file mode 100644 index 0000000..0e101a2 --- /dev/null +++ b/examples/playdate-project-example/Source/pdxinfo @@ -0,0 +1,5 @@ +name=Clay Playdate Example +author=Matthew Jennings +description=A small demo of Clay running on the Playdate +bundleID=dev.mattahj.clay_example +imagePath= diff --git a/examples/playdate-project-example/main.c b/examples/playdate-project-example/main.c new file mode 100644 index 0000000..45305d8 --- /dev/null +++ b/examples/playdate-project-example/main.c @@ -0,0 +1,103 @@ + +#include "pd_api.h" +#define CLAY_IMPLEMENTATION +#include "../../clay.h" + +#include "../../renderers/playdate/clay_renderer_playdate.c" +#include "../shared-layouts/clay-video-demo.c" + +static int update(void *userdata); +const char *fontpath = "/System/Fonts/Asheville-Sans-14-Bold.pft"; +LCDFont *font = NULL; + +void HandleClayErrors(Clay_ErrorData errorData) {} + +struct TextUserData { + LCDFont *font; + PlaydateAPI *pd; +}; + +static struct TextUserData gTextUserData = {.font = NULL, .pd = NULL}; +static ClayVideoDemo_Data demoData; + +static Clay_Dimensions PlayDate_MeasureText(Clay_StringSlice text, + Clay_TextElementConfig *config, + void *userData) { + // TODO: playdate needs to load fonts at the given size, so need to do that + // before we can use different font sizes! + struct TextUserData *textUserData = userData; + int width = textUserData->pd->graphics->getTextWidth( + textUserData->font, text.chars, + utf8_count_codepoints(text.chars, text.length), kUTF8Encoding, 0); + int height = textUserData->pd->graphics->getFontHeight(textUserData->font); + return (Clay_Dimensions){ + .width = (float)width, + .height = (float)height, + }; +} + +#ifdef _WINDLL +__declspec(dllexport) +#endif +int eventHandler(PlaydateAPI* pd, PDSystemEvent event, uint32_t arg) +{ + (void)arg; // arg is currently only used for event = kEventKeyPressed + + if (event == kEventInit) { + const char *err; + font = pd->graphics->loadFont(fontpath, &err); + + if (font == NULL) + pd->system->error("%s:%i Couldn't load font %s: %s", __FILE__, __LINE__, + fontpath, err); + + gTextUserData.pd = pd; + gTextUserData.font = font; + + // Note: If you set an update callback in the kEventInit handler, the system + // assumes the game is pure C and doesn't run any Lua code in the game + pd->system->setUpdateCallback(update, pd); + + uint64_t totalMemorySize = Clay_MinMemorySize(); + Clay_Arena clayMemory = Clay_CreateArenaWithCapacityAndMemory( + totalMemorySize, pd->system->realloc(NULL, totalMemorySize)); + Clay_Initialize(clayMemory, + (Clay_Dimensions){(float)pd->display->getWidth(), + (float)pd->display->getHeight()}, + (Clay_ErrorHandler){HandleClayErrors}); + Clay_SetMeasureTextFunction(PlayDate_MeasureText, &gTextUserData); + demoData = ClayVideoDemo_Initialize(pd); + } + + return 0; +} + +#define TEXT_WIDTH 86 +#define TEXT_HEIGHT 16 + +int x = (400 - TEXT_WIDTH) / 2; +int y = (240 - TEXT_HEIGHT) / 2; +int dx = 1; +int dy = 2; + +static int update(void *userdata) { + PlaydateAPI *pd = userdata; + + pd->graphics->clear(kColorWhite); + pd->graphics->setFont(font); + + Clay_SetPointerState((Clay_Vector2){.x = pd->display->getWidth() / 2.0f, + .y = pd->display->getHeight() / 2.0f}, + false); + float crankDelta = pd->system->getCrankChange(); + Clay_UpdateScrollContainers(true, (Clay_Vector2){0, crankDelta * 0.25f}, + pd->system->getElapsedTime()); + Clay_RenderCommandArray renderCommands = + ClayVideoDemo_CreateLayout(&demoData); + + Clay_Playdate_Render(pd, renderCommands, font); + + pd->system->drawFPS(0, 0); + + return 1; +} diff --git a/renderers/playdate/clay_renderer_playdate.c b/renderers/playdate/clay_renderer_playdate.c new file mode 100644 index 0000000..6bb7c10 --- /dev/null +++ b/renderers/playdate/clay_renderer_playdate.c @@ -0,0 +1,156 @@ +#include "../../clay.h" +#include "pd_api.h" + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +struct Rect { + float x; + float y; + float w; + float h; +}; + +static size_t utf8_count_codepoints(const char *str, size_t byte_len) { + size_t count = 0; + size_t i = 0; + while (i < byte_len) { + uint8_t c = (uint8_t)str[i]; + + // Skip continuation bytes (10xxxxxx) + if ((c & 0xC0) != 0x80) { + count++; + } + + i++; + } + return count; +} + +static LCDColor clayColorToLCDColor(Clay_Color color) { + if (color.r == 255 && color.g == 255 && color.b == 255) { + return kColorWhite; + } + return kColorBlack; +} + +static LCDBitmapDrawMode clayColorToDrawMode(Clay_Color color) { + + if (color.r == 255 && color.g == 255 && color.b == 255) { + return kDrawModeFillWhite; + } + return kDrawModeCopy; +} + +static void Clay_Playdate_Render(PlaydateAPI *pd, + Clay_RenderCommandArray renderCommands, + LCDFont *fonts) { + + for (uint32_t i = 0; i < renderCommands.length; i++) { + Clay_RenderCommand *renderCommand = + Clay_RenderCommandArray_Get(&renderCommands, i); + Clay_BoundingBox boundingBox = renderCommand->boundingBox; + switch (renderCommand->commandType) { + case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { + Clay_RectangleRenderData *config = &renderCommand->renderData.rectangle; + struct Rect rect = (struct Rect){ + .x = boundingBox.x, + .y = boundingBox.y, + .w = boundingBox.width, + .h = boundingBox.height, + }; + if (config->cornerRadius.topLeft > 0) { + pd->graphics->fillRoundRect( + rect.x, rect.y, rect.w, rect.h, config->cornerRadius.topLeft, + clayColorToLCDColor(config->backgroundColor)); + } else { + pd->graphics->fillRect(rect.x, rect.y, rect.w, rect.h, + clayColorToLCDColor(config->backgroundColor)); + } + break; + } + case CLAY_RENDER_COMMAND_TYPE_TEXT: { + Clay_TextRenderData *config = &renderCommand->renderData.text; + // LCDFont *font = fonts[config->fontId]; + // TODO: support loading more than 1 font and use the fonts that clay + // layout has.. + LCDFont *font = fonts; + struct Rect destination = (struct Rect){ + .x = boundingBox.x, + .y = boundingBox.y, + .w = boundingBox.width, + .h = boundingBox.height, + }; + pd->graphics->setDrawMode(clayColorToDrawMode(config->textColor)); + pd->graphics->drawText( + renderCommand->renderData.text.stringContents.chars, + utf8_count_codepoints( + renderCommand->renderData.text.stringContents.chars, + renderCommand->renderData.text.stringContents.length), + kUTF8Encoding, destination.x, destination.y); + pd->graphics->setDrawMode(kDrawModeCopy); + + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { + struct Rect currentClippingRectangle = (struct Rect){ + .x = boundingBox.x, + .y = boundingBox.y, + .w = boundingBox.width, + .h = boundingBox.height, + }; + pd->graphics->setClipRect( + currentClippingRectangle.x, currentClippingRectangle.y, + currentClippingRectangle.w, currentClippingRectangle.h); + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: { + pd->graphics->clearClipRect(); + break; + } + case CLAY_RENDER_COMMAND_TYPE_IMAGE: { + Clay_ImageRenderData *config = &renderCommand->renderData.image; + LCDBitmap *texture = config->imageData; + + struct Rect destination = (struct Rect){ + .x = boundingBox.x, + .y = boundingBox.y, + .w = boundingBox.width, + .h = boundingBox.height, + }; + + pd->graphics->drawBitmap(texture, destination.x, destination.y, + kBitmapUnflipped); + + break; + } + case CLAY_RENDER_COMMAND_TYPE_BORDER: { + Clay_BorderRenderData *config = &renderCommand->renderData.border; + + struct Rect rect = (struct Rect){ + .x = boundingBox.x, + .y = boundingBox.y, + .w = boundingBox.width, + .h = boundingBox.height, + }; + + // TODO: properly support the different border thickness and radius + // instead of just using topLeft corner /top thickness as a global setting + if (config->cornerRadius.topLeft > 0) { + pd->graphics->drawRoundRect( + rect.x, rect.y, rect.w, rect.h, config->cornerRadius.topLeft, + config->width.top, clayColorToLCDColor(config->color)); + } else { + pd->graphics->drawRect(rect.x, rect.y, rect.w, rect.h, + clayColorToLCDColor(config->color)); + } + break; + } + default: { + pd->system->logToConsole("Error: unhandled render command: %d\n", + renderCommand->commandType); + return; + } + } + } +}