From e2f94f17f6d2b0c5095c9309d24e7232f267d265 Mon Sep 17 00:00:00 2001 From: Matt Jennings Date: Mon, 19 May 2025 02:46:39 +0300 Subject: [PATCH] [Renderers/Playdate] Playdate console example (#404) --- CMakeLists.txt | 6 + examples/playdate-project-example/.gitignore | 3 + .../playdate-project-example/CmakeLists.txt | 40 ++ examples/playdate-project-example/README.md | 37 ++ .../playdate-project-example/Source/pdxinfo | 5 + .../playdate-project-example/Source/star.png | Bin 0 -> 4511 bytes .../clay-video-demo-playdate.c | 366 ++++++++++++++++++ examples/playdate-project-example/main.c | 115 ++++++ renderers/playdate/clay_renderer_playdate.c | 273 +++++++++++++ 9 files changed, 845 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/README.md create mode 100644 examples/playdate-project-example/Source/pdxinfo create mode 100644 examples/playdate-project-example/Source/star.png create mode 100644 examples/playdate-project-example/clay-video-demo-playdate.c 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..5b166fb 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}") @@ -43,6 +44,11 @@ if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_SOKOL_EXAMPLES) add_subdirectory("examples/sokol-corner-radius") 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) + 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) add_subdirectory("examples/win32_gdi") diff --git a/examples/playdate-project-example/.gitignore b/examples/playdate-project-example/.gitignore new file mode 100644 index 0000000..736b2f4 --- /dev/null +++ b/examples/playdate-project-example/.gitignore @@ -0,0 +1,3 @@ +clay_playdate_example.pdx +Source/pdex.dylib +Source/pdex.elf 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/README.md b/examples/playdate-project-example/README.md new file mode 100644 index 0000000..bdbe854 --- /dev/null +++ b/examples/playdate-project-example/README.md @@ -0,0 +1,37 @@ +# Playdate console example + +This example uses a modified version of the document viewer application from the Clay video demo. The Playdate console has a very small black and white screen, so some of the fixed sizes and styles needed to be modified to make the application usable on the console. The selected document can be changed using up/down on the D-pad, and the selected document can be scrolled with the crank. + +## Building + +You need to have the (Playdate SDK)[https://play.date/dev/] installed to be able to build this example. Once it's installed you can build it by adding -DCLAY_INCLUDE_PLAYDATE_EXAMPLES=ON when initialising a directory with cmake. + +e.g. + +``` +cmake -DCLAY_INCLUDE_PLAYDATE_EXAMPLES=ON cmake-build-debug +``` + +And then build it: + +``` +cmake --build cmake-build-debug +``` + +The pdx file will be located at examples/playdate-project-example/clay_playdate_example.pdx. You can then open it with the Playdate simulator. + +## Building for the playdate device + +By default building this example will produce a pdx which can only run on the Playdate simulator application. To build a pdx that can run on the Playdate hardware you need to set the toolchain to use armgcc, toolchain file to the arm.cmake provided in the playdate SDK and make sure to disable the other examples. The Playdate hardware requires threads to be disabled which is not compatible with some of the other examples. + +e.g. To setup the cmake-build-release directory for device builds: + +``` +cmake -DTOOLCHAIN=armgcc -DCMAKE_TOOLCHAIN_FILE=/Users/mattahj/Developer/PlaydateSDK/C_API/buildsupport/arm.cmake -DCLAY_INCLUDE_ALL_EXAMPLES=OFF -DCLAY_INCLUDE_PLAYDATE_EXAMPLES=ON -B cmake-build-release +``` + +And then build it: + +``` +cmake --build cmake-build-release +``` 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/Source/star.png b/examples/playdate-project-example/Source/star.png new file mode 100644 index 0000000000000000000000000000000000000000..1b33759871311d55e40725662082b2497b37ad35 GIT binary patch literal 4511 zcmeHKeNYtV8DB*N`7lOP6?N>oG$ESY-R|D)akt_4y5l~^EL`y#L`v)K-X3hZyFGUo z?w}JvjWr1~CXwKvp^YDyNz#xunXxg5A50;bF|CryBx*on;+Ry8n$g6DYWwcqfiS_g z%ryUTGkd%HywCG{pXc{{%)ZaK3mvIRi;^G+N_9DH#o!KuYiZ(4@J}TzSq^SJb)MC7 zF&&0OqTu8G94yy{IGBs@J_w4O3ApoS$1O|j9DX)?nyoyt_3VCE{mF}AkH6+@;dxU- z+J=+AIN>-@Z7W=Mfog2d&wbnbR{q1(sjkQQYq>Y(o?n?A+Iw@OD|9?%|E=NXi_dj+ zuJB~1eL0XkynpTcw{IRlR<>>Mryp%uykoJeHNIvO*}I47_^spPk7gv3MPIyl)$PpA00$fNOVy6q2jq|N_YC`$e`6}eh!+xtxr_&n_? z-fDHbtk#i$Kp3r0zF=|wId@6VFUuPDXlB>UsXlg+JY7(*r0Pe{x|f|wYA0KM{#`t^ zXxjq|99{c|hI*O)uAb78gp#~D|Af{pXnml(t@~dOZk~@VUGnW`1BZGC9y@yO4ZQ!d z3wqs`w!>3=>_W-r!IBwR1IjFHZh7s*Gt8pjeRgSS_Tj;v+U?)Io8wr$yd~w@tnOK_ zES`UT=JgL=*(VKVZ2jZgZF63pSF}*hJMVb&xc108t@-lB9rvfdv?F8doaBAl?FU<$ zQ=20@O|QSY=)$n1eWdjr)9?Gqs&h?8P|wLHw!d2_r7VAxnT5Ve zyKlE`sqent`1!W?e>N?>clxi~%gpPq<=h%*FKyi^H|4wFTg=%XW4po&DhdDa@W+2D z%g7x{JMbZSXU)8p+dnzf*SE)UbjTQ5H_-RFWk^g|xcKDVZBxmD|DFIY}TQ;4#zb>LnGA& zWjdk>O6dwj6vM_ztjLFCUI@YpCd~-dvIRlFIy~ZEAVg84@IfiY0^mazp+h=Mi|PUa z-B=4rE~o(@F^49!kUXGIb;X<{REsQEP{Re~^zjhI+})V|2MC}(4#3NfIuNQB{VE?` zR>%3d0N6_cZe!zocDN|_sD;8|1s@2hRv=sBG-cj*56_epm5;HJ0P|7Y3FAy5b8j$s7`v%!kx*)e;*$!;}Z*1R$1uAn5-L6#da z=YhE%XN{~8V>Bph<}?Nz)B){f2n|OQH0t#cCcTjx15qUMppiLS0ZuCiC1b3xaFWyArpb zmVLm$f>1OI7p=Ox;^70_Dw$R-&=b0(Te;Cj?($c*l!0zScs5~yDV6hpOoqiU|? zf&|e3Hfg200e~t8w31elqh&$#2!h{&C=jq>Il4@NCiK!WZKGulfTB1-q6CRy9?VGU z%_N3Dg6c_hjK1LIeYO8hTWL8sE1G8~FM<7QRZ+ALS8?m2Z_!sjuQnhItF1)RY%~Ok zuHn3@p8zYm#8%S53J%PiSdEUz`3Y))Fk!UW;4^E?go)J{FxITpLZ*hm^th43jWlB> z#-mGuPY%-}msbIJ1YCihP`QF1j%I54c=d24r=$Tera=kK_Zid0BGxIRW+db+-S_2u1piHr!h?-cJq4%^`UC&Y-A2TySJW&~##bC7S zMrV60naULX0e`U~`2lwT&{LaCir*=^rs$d!1Cvsos;()zCdI&{l&7le|3+8R-P0i# z1fOSNaQ15)eB&}WH_l{MI&A6*68eYrU_E%66>^qJ5R^Pmxu!uoTNeUhg6yIS626R2 zPR&R$_iz3U5Jg4^?!Lk|DWeY<_q^C^eFc0(3z-1Gw#iKW_I{ra}e?!=Wtr{dOp zQFm^Iwkp3Pu6{$;U(3o8uIb%GQ~6pw^!M+|7qrLE#e9uHT=CjfP-uHIf9=M910B{% Avj6}9 literal 0 HcmV?d00001 diff --git a/examples/playdate-project-example/clay-video-demo-playdate.c b/examples/playdate-project-example/clay-video-demo-playdate.c new file mode 100644 index 0000000..2d062ca --- /dev/null +++ b/examples/playdate-project-example/clay-video-demo-playdate.c @@ -0,0 +1,366 @@ +// This is the video demo with some adjustments so it works on the playdate +// console The playdate screen is only 400x240 pixels and it can only display +// black and white, so some fixed sizes and colours needed tweaking! The file +// menu was also removed as it does not really make sense when there is no +// pointer +// +// Note: The playdate console also does not support dynamic font sizes - fonts must be +// created at a specific size with the pdc tool - so any font size set in the clay layout +// will have no effect. +#include "pd_api.h" +#include "../../clay.h" +#include + +const int FONT_ID_BODY = 0; +const int FONT_ID_BUTTON = 1; + +Clay_Color COLOR_WHITE = { 255, 255, 255, 255 }; +Clay_Color COLOR_BLACK = { 0, 0, 0, 255 }; + +void RenderHeaderButton(Clay_String text) { + CLAY({ + .layout = { .padding = { 8, 8, 4, 4 } }, + .backgroundColor = COLOR_BLACK, + .cornerRadius = CLAY_CORNER_RADIUS(4) + }) { + CLAY_TEXT( + text, + CLAY_TEXT_CONFIG({ .fontId = FONT_ID_BUTTON, .textColor = COLOR_WHITE }) + ); + } +} + +typedef struct { + Clay_String title; + Clay_String contents; + LCDBitmap* image; +} Document; + +typedef struct { + Document *documents; + uint32_t length; +} DocumentArray; + +#define MAX_DOCUMENTS 3 +Document documentsRaw[MAX_DOCUMENTS]; + +DocumentArray documents = { .length = MAX_DOCUMENTS, .documents = documentsRaw }; + +void ClayVideoDemoPlaydate_Initialize(PlaydateAPI* pd) { + documents.documents[0] = (Document){ + .title = CLAY_STRING("Squirrels"), + .image = pd->graphics->loadBitmap("star.png", NULL), + .contents = CLAY_STRING( + "The Secret Life of Squirrels: Nature's Clever Acrobats\n" + "Squirrels are often overlooked creatures, dismissed as mere park " + "inhabitants or backyard nuisances. Yet, beneath their fluffy tails " + "and twitching noses lies an intricate world of cunning, agility, " + "and survival tactics that are nothing short of fascinating. As one " + "of the most common mammals in North America, squirrels have adapted " + "to a wide range of environments from bustling urban centers to " + "tranquil forests and have developed a variety of unique behaviors " + "that continue to intrigue scientists and nature enthusiasts alike.\n" + "\n" + "Master Tree Climbers\n" + "At the heart of a squirrel's skill set is its impressive ability to " + "navigate trees with ease. Whether they're darting from branch to " + "branch or leaping across wide gaps, squirrels possess an innate " + "talent for acrobatics. Their powerful hind legs, which are longer " + "than their front legs, give them remarkable jumping power. With a " + "tail that acts as a counterbalance, squirrels can leap distances of " + "up to ten times the length of their body, making them some of the " + "best aerial acrobats in the animal kingdom.\n" + "But it's not just their agility that makes them exceptional " + "climbers. Squirrels' sharp, curved claws allow them to grip tree " + "bark with precision, while the soft pads on their feet provide " + "traction on slippery surfaces. Their ability to run at high speeds " + "and scale vertical trunks with ease is a testament to the " + "evolutionary adaptations that have made them so successful in their " + "arboreal habitats.\n" + "\n" + "Food Hoarders Extraordinaire\n" + "Squirrels are often seen frantically gathering nuts, seeds, and " + "even fungi in preparation for winter. While this behavior may seem " + "like instinctual hoarding, it is actually a survival strategy that " + "has been honed over millions of years. Known as \"scatter " + "hoarding,\" squirrels store their food in a variety of hidden " + "locations, often burying it deep in the soil or stashing it in " + "hollowed-out tree trunks.\n" + "Interestingly, squirrels have an incredible memory for the " + "locations of their caches. Research has shown that they can " + "remember thousands of hiding spots, often returning to them months " + "later when food is scarce. However, they don't always recover every " + "stash some forgotten caches eventually sprout into new trees, " + "contributing to forest regeneration. This unintentional role as " + "forest gardeners highlights the ecological importance of squirrels " + "in their ecosystems.\n" + "\n" + "The Great Squirrel Debate: Urban vs. Wild\n" + "While squirrels are most commonly associated with rural or wooded " + "areas, their adaptability has allowed them to thrive in urban " + "environments as well. In cities, squirrels have become adept at " + "finding food sources in places like parks, streets, and even " + "garbage cans. However, their urban counterparts face unique " + "challenges, including traffic, predators, and the lack of natural " + "shelters. Despite these obstacles, squirrels in urban areas are " + "often observed using human infrastructure such as buildings, " + "bridges, and power lines as highways for their acrobatic " + "escapades.\n" + "There is, however, a growing concern regarding the impact of urban " + "life on squirrel populations. Pollution, deforestation, and the " + "loss of natural habitats are making it more difficult for squirrels " + "to find adequate food and shelter. As a result, conservationists " + "are focusing on creating squirrel-friendly spaces within cities, " + "with the goal of ensuring these resourceful creatures continue to " + "thrive in both rural and urban landscapes.\n" + "\n" + "A Symbol of Resilience\n" + "In many cultures, squirrels are symbols of resourcefulness, " + "adaptability, and preparation. Their ability to thrive in a variety " + "of environments while navigating challenges with agility and grace " + "serves as a reminder of the resilience inherent in nature. Whether " + "you encounter them in a quiet forest, a city park, or your own " + "backyard, squirrels are creatures that never fail to amaze with " + "their endless energy and ingenuity.\n" + "In the end, squirrels may be small, but they are mighty in their " + "ability to survive and thrive in a world that is constantly " + "changing. So next time you spot one hopping across a branch or " + "darting across your lawn, take a moment to appreciate the " + "remarkable acrobat at work a true marvel of the natural world.\n" + ) + }; + documents.documents[1] = (Document){ + .title = CLAY_STRING("Lorem Ipsum"), + .image = pd->graphics->loadBitmap("star.png", NULL), + .contents = CLAY_STRING( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " + "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim " + "ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + "aliquip ex ea commodo consequat. Duis aute irure dolor in " + "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " + "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum." + ) + }; + documents.documents[2] = (Document){ + .title = CLAY_STRING("Vacuum Instructions"), + .image = pd->graphics->loadBitmap("star.png", NULL), + .contents = CLAY_STRING( + "Chapter 3: Getting Started - Unpacking and Setup\n" + "\n" + "Congratulations on your new SuperClean Pro 5000 vacuum cleaner! In " + "this section, we will guide you through the simple steps to get " + "your vacuum up and running. Before you begin, please ensure that " + "you have all the components listed in the \"Package Contents\" " + "section on page 2.\n" + "\n" + "1. Unboxing Your Vacuum\n" + "Carefully remove the vacuum cleaner from the box. Avoid using sharp " + "objects that could damage the product. Once removed, place the unit " + "on a flat, stable surface to proceed with the setup. Inside the " + "box, you should find:\n" + "\n" + " The main vacuum unit\n" + " A telescoping extension wand\n" + " A set of specialized cleaning tools (crevice tool, upholstery " + "brush, etc.)\n" + " A reusable dust bag (if applicable)\n" + " A power cord with a 3-prong plug\n" + " A set of quick-start instructions\n" + "\n" + "2. Assembling Your Vacuum\n" + "Begin by attaching the extension wand to the main body of the " + "vacuum cleaner. Line up the connectors and twist the wand into " + "place until you hear a click. Next, select the desired cleaning " + "tool and firmly attach it to the wand's end, ensuring it is " + "securely locked in.\n" + "\n" + "For models that require a dust bag, slide the bag into the " + "compartment at the back of the vacuum, making sure it is properly " + "aligned with the internal mechanism. If your vacuum uses a bagless " + "system, ensure the dust container is correctly seated and locked in " + "place before use.\n" + "\n" + "3. Powering On\n" + "To start the vacuum, plug the power cord into a grounded electrical " + "outlet. Once plugged in, locate the power switch, usually " + "positioned on the side of the handle or body of the unit, depending " + "on your model. Press the switch to the \"On\" position, and you " + "should hear the motor begin to hum. If the vacuum does not power " + "on, check that the power cord is securely plugged in, and ensure " + "there are no blockages in the power switch.\n" + "\n" + "Note: Before first use, ensure that the vacuum filter (if your " + "model has one) is properly installed. If unsure, refer to \"Section " + "5: Maintenance\" for filter installation instructions." + ) + }; +} + +Clay_RenderCommandArray ClayVideoDemoPlaydate_CreateLayout(int selectedDocumentIndex) { + + Clay_BeginLayout(); + + Clay_Sizing layoutExpand = { + .width = CLAY_SIZING_GROW(0), + .height = CLAY_SIZING_GROW(0) + }; + + Clay_BorderElementConfig contentBorders = { + .color = COLOR_BLACK, + .width = { .top = 1, .left = 1, .right = 1, .bottom = 1 } + }; + + // Build UI here + CLAY({ + .id = CLAY_ID("OuterContainer"), + .backgroundColor = COLOR_WHITE, + .layout = { + .layoutDirection = CLAY_TOP_TO_BOTTOM, + .sizing = layoutExpand, + .padding = CLAY_PADDING_ALL(8), + .childGap = 4 + } + }) { + // Child elements go inside braces + CLAY({ + .id = CLAY_ID("HeaderBar"), + .layout = { + .sizing = { + .height = CLAY_SIZING_FIXED(30), + .width = CLAY_SIZING_GROW(0) + }, + .childGap = 8, + .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } + }, + }) { + // Header buttons go here + CLAY({ + .id = CLAY_ID("FileButton"), + .layout = { + .padding = { 8, 8, 4, 4 } + }, + .backgroundColor = COLOR_BLACK, + .cornerRadius = CLAY_CORNER_RADIUS(4) + }) { + CLAY_TEXT( + CLAY_STRING("File"), + CLAY_TEXT_CONFIG({ + .fontId = FONT_ID_BUTTON, + .textColor = COLOR_WHITE + }) + ); + } + RenderHeaderButton(CLAY_STRING("Edit")); + CLAY({ .layout = { .sizing = { CLAY_SIZING_GROW(0) } } }) {} + RenderHeaderButton(CLAY_STRING("Upload")); + RenderHeaderButton(CLAY_STRING("Media")); + RenderHeaderButton(CLAY_STRING("Support")); + } + + CLAY({ + .id = CLAY_ID("LowerContent"), + .layout = { .sizing = layoutExpand, .childGap = 8 }, + }) { + CLAY({ + .id = CLAY_ID("Sidebar"), + .border = contentBorders, + .cornerRadius = CLAY_CORNER_RADIUS(4), + .layout = { + .layoutDirection = CLAY_TOP_TO_BOTTOM, + .padding = CLAY_PADDING_ALL(8), + .childGap = 4, + .sizing = { + .width = CLAY_SIZING_FIXED(125), + .height = CLAY_SIZING_GROW(0) + } + } + }) { + for (int i = 0; i < documents.length; i++) { + Document document = documents.documents[i]; + Clay_LayoutConfig sidebarButtonLayout = { + .sizing = { .width = CLAY_SIZING_GROW(0) }, + .padding = CLAY_PADDING_ALL(8) + }; + + if (i == selectedDocumentIndex) { + CLAY({ + .layout = sidebarButtonLayout, + .backgroundColor = COLOR_BLACK, + .cornerRadius = CLAY_CORNER_RADIUS(4) + }) { + CLAY_TEXT( + document.title, + CLAY_TEXT_CONFIG({ + .fontId = FONT_ID_BUTTON, + .textColor = COLOR_WHITE + }) + ); + } + } else { + CLAY({ + .layout = sidebarButtonLayout, + .backgroundColor = (Clay_Color){ 0, 0, 0, Clay_Hovered() ? 120 : 0 }, + .cornerRadius = CLAY_CORNER_RADIUS(4), + .border = contentBorders + }) { + CLAY_TEXT( + document.title, + CLAY_TEXT_CONFIG({ + .fontId = FONT_ID_BUTTON, + .textColor = COLOR_BLACK, + }) + ); + } + } + } + } + + CLAY({ + .id = CLAY_ID("MainContent"), + .border = contentBorders, + .cornerRadius = CLAY_CORNER_RADIUS(4), + .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() }, + .layout = { + .layoutDirection = CLAY_TOP_TO_BOTTOM, + .childGap = 8, + .padding = CLAY_PADDING_ALL(8), + .sizing = layoutExpand + } + }) { + Document selectedDocument = documents.documents[selectedDocumentIndex]; + CLAY({ + .layout = { + .layoutDirection = CLAY_LEFT_TO_RIGHT, + .childGap = 4, + .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_BOTTOM } + } + }) { + CLAY_TEXT( + selectedDocument.title, + CLAY_TEXT_CONFIG({ .fontId = FONT_ID_BODY, .textColor = COLOR_BLACK }) + ); + CLAY({ + .layout = { + .sizing = { + .width = CLAY_SIZING_FIXED(32), + .height = CLAY_SIZING_FIXED(30) + } + }, + .image = { .imageData = selectedDocument.image, .sourceDimensions = { 32, 30 } } + }) {} + } + CLAY_TEXT( + selectedDocument.contents, + CLAY_TEXT_CONFIG({ .fontId = FONT_ID_BODY, .textColor = COLOR_BLACK }) + ); + } + } + } + + Clay_RenderCommandArray renderCommands = Clay_EndLayout(); + for (int32_t i = 0; i < renderCommands.length; i++) { + Clay_RenderCommandArray_Get(&renderCommands, i); + } + return renderCommands; +} diff --git a/examples/playdate-project-example/main.c b/examples/playdate-project-example/main.c new file mode 100644 index 0000000..1aaaaa3 --- /dev/null +++ b/examples/playdate-project-example/main.c @@ -0,0 +1,115 @@ +#include "pd_api.h" +#define CLAY_IMPLEMENTATION +#include "../../clay.h" + +#include "../../renderers/playdate/clay_renderer_playdate.c" +#include "clay-video-demo-playdate.c" + +static int update(void *userdata); + +#define NUM_FONTS 2 +const char *fontsToLoad[NUM_FONTS] = { + "/System/Fonts/Asheville-Sans-14-Bold.pft", + "/System/Fonts/Roobert-10-Bold.pft" +}; + +void HandleClayErrors(Clay_ErrorData errorData) {} + +struct TextUserData { + LCDFont *font[NUM_FONTS]; + PlaydateAPI *pd; +}; + +static struct TextUserData textUserData = { .font = { NULL }, .pd = NULL }; + +static Clay_Dimensions PlayDate_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) { + struct TextUserData *textUserData = userData; + int width = textUserData->pd->graphics->getTextWidth( + textUserData->font[config->fontId], + text.chars, + Clay_Playdate_CountUtf8Codepoints(text.chars, text.length), + kUTF8Encoding, + 0 + ); + int height = textUserData->pd->graphics->getFontHeight(textUserData->font[config->fontId]); + return (Clay_Dimensions){ + .width = (float)width, + .height = (float)height, + }; +} + +#ifdef _WINDLL +__declspec(dllexport) +#endif +int eventHandler(PlaydateAPI* pd, PDSystemEvent event, uint32_t eventArg) { + if (event == kEventInit) { + const char *err; + for (int i = 0; i < NUM_FONTS; ++i) { + + textUserData.font[i] = pd->graphics->loadFont(fontsToLoad[i], &err); + if (textUserData.font[i] == NULL) { + pd->system->error("%s:%i Couldn't load font %s: %s", __FILE__, __LINE__, fontsToLoad[i], err); + } + } + + textUserData.pd = pd; + 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, &textUserData); + ClayVideoDemoPlaydate_Initialize(pd); + } + + return 0; +} + +int selectedDocumentIndex = 0; +#define WRAP_RANGE(x, N) ((((x) % (N)) + (N)) % (N)) + +static int update(void *userdata) { + PlaydateAPI *pd = userdata; + PDButtons pushedButtons; + pd->system->getButtonState(NULL, &pushedButtons, NULL); + + if (pushedButtons & kButtonDown) { + selectedDocumentIndex = WRAP_RANGE(selectedDocumentIndex + 1, MAX_DOCUMENTS); + } else if (pushedButtons & kButtonUp) { + selectedDocumentIndex = WRAP_RANGE(selectedDocumentIndex - 1, MAX_DOCUMENTS); + } + + pd->graphics->clear(kColorWhite); + + // A bit hacky, setting the cursor on to the document view so it can be + // scrolled.. + Clay_SetPointerState( + (Clay_Vector2){ + .x = pd->display->getWidth() / 2.0f, + .y = pd->display->getHeight() / 2.0f + }, + false + ); + + float crankDelta = pd->system->getCrankChange(); + Clay_UpdateScrollContainers( + false, + (Clay_Vector2){ 0, -crankDelta * 0.25f }, + pd->system->getElapsedTime() + ); + + Clay_RenderCommandArray renderCommands = ClayVideoDemoPlaydate_CreateLayout(selectedDocumentIndex); + Clay_Playdate_Render(pd, renderCommands, textUserData.font); + + return 1; +} diff --git a/renderers/playdate/clay_renderer_playdate.c b/renderers/playdate/clay_renderer_playdate.c new file mode 100644 index 0000000..e6c5c85 --- /dev/null +++ b/renderers/playdate/clay_renderer_playdate.c @@ -0,0 +1,273 @@ +#include "pd_api.h" +#include "../../clay.h" + +// Playdate drawText function expects the number of codepoints to draw, not byte length +static size_t Clay_Playdate_CountUtf8Codepoints(const char *str, size_t byteLen) { + size_t count = 0; + size_t i = 0; + while (i < byteLen) { + uint8_t c = (uint8_t)str[i]; + if ((c & 0xC0) != 0x80) { + count++; + } + i++; + } + return count; +} + +// As the playdate can only display black and white, we need to resolve Clay_color to either black or white +// for both color and draw mode. +static LCDColor clayColorToLCDColor(Clay_Color color) { + if (color.r > 0 || color.g > 0 || color.b > 0) { + return kColorWhite; + } + return kColorBlack; +} + +static LCDBitmapDrawMode clayColorToDrawMode(Clay_Color color) { + if (color.r > 0 || color.g > 0 || color.b > 0) { + return kDrawModeFillWhite; + } + return kDrawModeCopy; +} + +static float clampCornerRadius(float yAxisSize, float radius) { + if (radius < 1.0f) { + return 0.0f; + } + if (radius > yAxisSize / 2) { + return yAxisSize / 2; + } + // Trying to draw a 2x2 ellipse seems to result in just a dot, so if + // there is a corner radius at minimum it must be 2 + return CLAY__MAX(2, radius); +} + +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; + + float radiusTl = clampCornerRadius(boundingBox.height, config->cornerRadius.topLeft); + float radiusTr = clampCornerRadius(boundingBox.height, config->cornerRadius.topRight); + float radiusBl = clampCornerRadius(boundingBox.height, config->cornerRadius.bottomLeft); + float radiusBr = clampCornerRadius(boundingBox.height, config->cornerRadius.bottomRight); + + pd->graphics->fillEllipse( + boundingBox.x, boundingBox.y, + radiusTl * 2, radiusTl * 2, + -90.0f, 0.0f, + clayColorToLCDColor(config->backgroundColor) + ); + + pd->graphics->fillEllipse( + boundingBox.x + boundingBox.width - radiusTr * 2, boundingBox.y, + radiusTr * 2, radiusTr * 2, + 0.0f, 90.0f, + clayColorToLCDColor(config->backgroundColor) + ); + + pd->graphics->fillEllipse( + boundingBox.x + boundingBox.width - radiusBr * 2, + boundingBox.y + boundingBox.height - radiusBr * 2, + radiusBr * 2, radiusBr * 2, + 90.0f, 180.0f, + clayColorToLCDColor(config->backgroundColor) + ); + + pd->graphics->fillEllipse( + boundingBox.x, + boundingBox.y + boundingBox.height - radiusBl * 2, + radiusBl * 2, radiusBl * 2, + 180.0f, 270.0f, + clayColorToLCDColor(config->backgroundColor) + ); + + // Top chunk + pd->graphics->fillRect( + boundingBox.x + radiusTl, boundingBox.y, + boundingBox.width - radiusTl - radiusTr, + CLAY__MAX(radiusTl, radiusTr), + clayColorToLCDColor(config->backgroundColor) + ); + + // bottom chunk + int bottomChunkHeight = CLAY__MAX(radiusBl, radiusBr); + pd->graphics->fillRect( + boundingBox.x + radiusBl, boundingBox.y + boundingBox.height - bottomChunkHeight, + boundingBox.width - radiusBl - radiusBr, + bottomChunkHeight, + clayColorToLCDColor(config->backgroundColor) + ); + + // Middle chunk + int middleChunkHeight = boundingBox.height - CLAY__MIN(radiusBr, radiusBl) - CLAY__MIN(radiusTr, radiusTl); + pd->graphics->fillRect( + boundingBox.x + CLAY__MIN(radiusTl, radiusBl), boundingBox.y + CLAY__MIN(radiusTr, radiusTl), + boundingBox.width - radiusBl - radiusBr, + middleChunkHeight, + clayColorToLCDColor(config->backgroundColor) + ); + + // Left chunk + int leftChunkHeight = boundingBox.height - radiusTl - radiusBl; + int leftChunkWidth = CLAY__MAX(radiusTl, radiusBl); + pd->graphics->fillRect( + boundingBox.x, boundingBox.y + radiusTl, + leftChunkWidth, + leftChunkHeight, + clayColorToLCDColor(config->backgroundColor) + ); + + // Right chunk + int rightChunkHeight = boundingBox.height - radiusTr - radiusBr; + int rightChunkWidth = CLAY__MAX(radiusTr, radiusBr); + pd->graphics->fillRect( + boundingBox.x + boundingBox.width - rightChunkWidth, boundingBox.y + radiusTr, + rightChunkWidth, + rightChunkHeight, + clayColorToLCDColor(config->backgroundColor) + ); + break; + } + case CLAY_RENDER_COMMAND_TYPE_TEXT: { + Clay_TextRenderData *config = &renderCommand->renderData.text; + LCDFont *font = fonts[config->fontId]; + pd->graphics->setFont(font); + pd->graphics->setDrawMode(clayColorToDrawMode(config->textColor)); + pd->graphics->drawText( + renderCommand->renderData.text.stringContents.chars, + Clay_Playdate_CountUtf8Codepoints( + renderCommand->renderData.text.stringContents.chars, + renderCommand->renderData.text.stringContents.length + ), + kUTF8Encoding, + boundingBox.x, + boundingBox.y + ); + pd->graphics->setDrawMode(kDrawModeCopy); + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { + pd->graphics->setClipRect( + boundingBox.x,boundingBox.y, + boundingBox.width, boundingBox.height + ); + 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; + int texWidth; + int texHeight; + pd->graphics->getBitmapData(texture, &texWidth, &texHeight, NULL, NULL, NULL); + if (texWidth != boundingBox.width || texHeight != boundingBox.height) { + pd->graphics->drawScaledBitmap( + texture, + boundingBox.x, boundingBox.y, + boundingBox.width / texWidth, + boundingBox.height / texHeight + ); + } else { + pd->graphics->drawBitmap(texture, boundingBox.x, boundingBox.y, kBitmapUnflipped); + } + break; + } + case CLAY_RENDER_COMMAND_TYPE_BORDER: { + Clay_BorderRenderData *config = &renderCommand->renderData.border; + + float radiusTl = clampCornerRadius(boundingBox.height, config->cornerRadius.topLeft); + float radiusTr = clampCornerRadius(boundingBox.height, config->cornerRadius.topRight); + float radiusBl = clampCornerRadius(boundingBox.height, config->cornerRadius.bottomLeft); + float radiusBr = clampCornerRadius(boundingBox.height, config->cornerRadius.bottomRight); + + if (config->width.top > 0) { + pd->graphics->drawEllipse( + boundingBox.x, boundingBox.y, + radiusTl * 2, radiusTl * 2, + config->width.top, + -90.0f, 0.0f, + clayColorToLCDColor(config->color) + ); + + pd->graphics->drawLine( + boundingBox.x + radiusTl, boundingBox.y, + boundingBox.x + boundingBox.width - radiusTr - config->width.right, boundingBox.y, + config->width.top, + clayColorToLCDColor(config->color) + ); + + pd->graphics->drawEllipse( + boundingBox.x + boundingBox.width - radiusTr * 2, boundingBox.y, + radiusTr * 2, radiusTr * 2, + config->width.top, + 0.0f, 90.0f, + clayColorToLCDColor(config->color) + ); + } + + if (config->width.right > 0 && radiusTr + radiusBr <= boundingBox.height) { + pd->graphics->drawLine( + boundingBox.x + boundingBox.width - config->width.right, + boundingBox.y + radiusTr, + boundingBox.x + boundingBox.width - config->width.right, + boundingBox.y + boundingBox.height - radiusBr - config->width.bottom, + config->width.right, + clayColorToLCDColor(config->color) + ); + } + + if (config->width.bottom > 0) { + pd->graphics->drawEllipse( + boundingBox.x + boundingBox.width - radiusBr * 2, + boundingBox.y + boundingBox.height - radiusBr * 2, + radiusBr * 2, radiusBr * 2, + config->width.bottom, + 90.0f, 180.0f, + clayColorToLCDColor(config->color) + ); + + pd->graphics->drawLine( + boundingBox.x + boundingBox.width - radiusBr - config->width.right, + boundingBox.y + boundingBox.height - config->width.bottom, + boundingBox.x + radiusBl, + boundingBox.y + boundingBox.height - config->width.bottom, + config->width.bottom, + clayColorToLCDColor(config->color) + ); + + pd->graphics->drawEllipse( + boundingBox.x, + boundingBox.y + boundingBox.height - radiusBl * 2, + radiusBl * 2, radiusBl * 2, + config->width.bottom, + 180.0f, 270.0f, + clayColorToLCDColor(config->color) + ); + } + + if (config->width.left > 0 && radiusBl + radiusTl < boundingBox.height) { + pd->graphics->drawLine( + boundingBox.x, boundingBox.y + boundingBox.height - radiusBl - config->width.bottom, + boundingBox.x, boundingBox.y + radiusTl, + config->width.left, + clayColorToLCDColor(config->color) + ); + } + break; + } + default: { + pd->system->logToConsole("Error: unhandled render command: %d\n", renderCommand->commandType); + return; + } + } + } +} -- 2.39.5