basics working on the playdate

This commit is contained in:
Matthew Jennings 2025-05-05 18:05:53 +03:00
parent 52759cd028
commit bed2d5b328
6 changed files with 310 additions and 0 deletions

View file

@ -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_SDL3_EXAMPLES "Build SDL 3 examples" OFF)
option(CLAY_INCLUDE_WIN32_GDI_EXAMPLES "Build Win32 GDI 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_SOKOL_EXAMPLES "Build Sokol examples" OFF)
option(CLAY_INCLUDE_PLAYDATE_EXAMPLES "Build Playdate examples" OFF)
message(STATUS "CLAY_INCLUDE_DEMOS: ${CLAY_INCLUDE_DEMOS}") 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-video-demo")
add_subdirectory("examples/sokol-corner-radius") add_subdirectory("examples/sokol-corner-radius")
endif() 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(WIN32) # Build only for Win or Wine
if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_WIN32_GDI_EXAMPLES) if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_WIN32_GDI_EXAMPLES)

View file

@ -0,0 +1,2 @@
clay_playdate_example.pdx
Source/pdex.dylib

View file

@ -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)

View file

@ -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=

View file

@ -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;
}

View file

@ -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;
}
}
}
}