feat: setup template

feat: implemented game of life

feat: implemented automatic stepping

fix: operator< Cell Cell now sorts by y first, then x

fix: equalized scroll x/y

fix: removed unused code path

fix: marked simulation::living as static since it's no longer forward
declared

tweak: decreased number of cells on random initialization

feat: defined Toggle element

feat: added debug information toggle to UI

feat: implemented simulation multithreading

feat: improved simulation frame delta controls

feat: inverted direction of panel colours

fix: removed leftover die colors

feat: reorganized and cleaned up style namespace

chore: increased button border width

feat: added SDL3 submodule

feat: added SDL3_ttf submodule

feat: moved clay to vendor (rather than include)

feat: replaced premake5 with CMake to vendor SDL3

chore: translated more C stuff to C++ in main.cpp

fix: stepping being inconsistent while running

feat: fixed incorrect behaviour on simulation and added benchmarking
code

feat: minor adjustments to UI layout

feat: increased thread count for pool

feat: improved simulation benchmarking

feat: simulation tasks can now be subdivided into separate workloads for
separate threads

feat: improved task counting thread safety

chore: massively increased random field

fix: target delta time is now enforced

feat: added toggle for locked framerate

fix: replaced manual .lock()/.unlock() calls with std::scoped_lock

fix: benchmarking code was off by a magnitude of 1

feat: separated cell state checking into separate function in case
another algo is faster than .contains

chore: some comments on variables in simulation.cpp

feat: set task split to hardware_concurrency()

feat: added basic culling to cell renderer

feat: implemented simulation threading toggle

chore: lowered random button's field size

chore: reduced padding on panel containers

chore: minor formatting adjustment

feat: added README.md

fix: inverted x scroll

feat: converted project to template

feat: added .clangd
This commit is contained in:
Sara 2025-09-22 00:06:01 +02:00 committed by Sara
commit 9b2ae482f1
27 changed files with 5622 additions and 0 deletions

2
.clangd Normal file
View file

@ -0,0 +1,2 @@
CompileFlags:
Add: [ -Ivendor/, -Ivendor/SDL3/include, -Ivendor/SDL3_ttf/include ]

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
bin/
build/
Makefile
.cache/
compile_commands.json
.kdev4
game-of-life.kdev4

6
.gitmodules vendored Normal file
View file

@ -0,0 +1,6 @@
[submodule "vendor/SDL3"]
path = vendor/SDL3
url = https://github.com/libsdl-org/SDL.git
[submodule "vendor/SDL3_ttf"]
path = vendor/SDL3_ttf
url = https://github.com/libsdl-org/SDL_ttf.git

25
CMakeLists.txt Normal file
View file

@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.21)
project(CHANGEME)
set(CMAKE_BINARY_DIR "${CMAKE_SOURCE_DIR}/bin")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_C_STANDARD 23)
file(GLOB_RECURSE source_files . src/**.cpp src/**.c vendor/renderer/**.c)
include_directories(vendor/)
add_subdirectory(vendor/SDL3/ EXCLUDE_FROM_ALL)
set(SDLTTF_VENDORED ON)
add_subdirectory(vendor/SDL3_ttf/ EXCLUDE_FROM_ALL)
add_executable(CHANGEME ${source_files})
target_link_libraries(CHANGEME PRIVATE SDL3_ttf::SDL3_ttf SDL3::SDL3)
add_custom_target(copy_assets
COMMAND ${CMAKE_COMMAND} -E
copy_directory ${CMAKE_SOURCE_DIR}/assets/ ${CMAKE_BINARY_DIR}/assets
)
add_dependencies(CHANGEME copy_assets)

119
README.md Normal file
View file

@ -0,0 +1,119 @@
# Clay SDL3 Template
## Using Template
### Just use [`just`](https://just.systems/man/en/)
Run `just set-project-name {project name}` first.
And remember to `git remote set-url origin {repo}`.
### Otherwise
Modify the CMakeLists.txt manually, just replace CHANGEME with whatever your binary should be called.
## Compiling
> Remember to `git submodule update --init --recursive`!
### Just again
`just configure` runs cmake configuration and generates a `compile_commands.json`.
`just build` runs the cmake build.
### CMake works too, I guess
`cmake -S. -Bbuild`
`cmake --build build`
Same as always
## Files
### thread_pool.h/cpp
Thread pool used for parallel GoL generation ticking.
### application.h/cpp
UI and layout logic.
### style.h/cpp
Reusable UI styling
### resources.h/cpp
Font loading.
### elements.h/cpp
Reusable UI elements
### input.h/cpp
Handling of input events.
### main.cpp:
Entrypoint, setup, and main application loop.
## Prerequisites
* CMake 3.21 or higher
* Compiler capable of C++23 and C23.
## Dependencies
### SDL3
Included as git submodule at `vendor/SDL3/` and dynamically linked.
### SDL3_ttf
Included as git submodule at `vendor/SDL3_ttf/` and dynamically linked.
### Clay
Included as files in `vendor/clay/clay.h`; Single header library.
### Clay SDL3 renderer
Included as files in `vendor/renderer/` and compiled as part of the project.
> Note: Mildly modified from the official Clay_SDL3_renderer to enable more advanced styling.
## Code Standards
* Keep program structure as simple as possible. No `class Application {` class or other Java-isms. Prefer namespaces with static lifetime variables.
* Use STL where possible. Don't reinvent the wheel.
* K&R brackets. With notable exceptions(1)
* camelCase for variables, PascalCase for types and functions (that's what SDL and Clay do).
* In class member functions, always use `this->` to access member variables.
* const applies to the name to it's left, so it goes after the type, not `const int x;` but `int const x`.
* \* and & flush with the declaration name. `Type const &Function(Type &inRef)`
> (1) Bracket exceptions:
> * using scoped_lock in arbitrary blocks, prefer tailed lisp brackets
```
struct Data {
int x{ 0 }, y{ 0 };
};
void MyFunction(Data const &data) { // K&R here
DoAsynchronousThings();
{ scoped_lock lock{ myMutex };
myVariable++;
} // not quite lisp, lisp with a tail
DoMoreAsynchronousThings();
}
```

Binary file not shown.

19
justfile Normal file
View file

@ -0,0 +1,19 @@
build:
# BUILDING
cmake --build build
run:
cd bin/ && CHANGEME
configure:
# CONFIGURING WITH PREMAKE
cmake -S. -Bbuild -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
clean:
# CLEANING BUILD ARTEFACTS
rm -r bin/**
rm -r build/**
set-project-name projectname: clean
git remote set-url origin ""
sed -i "s/CHANGEME/{{projectname}}/g" ./CMakeLists.txt ./justfile

28
src/application.cpp Normal file
View file

@ -0,0 +1,28 @@
#include "application.h"
#include "style.h"
#include "elements.h"
#include <SDL3/SDL.h>
#include <clay/clay.h>
namespace application {
static void SampleHeader() {
elements::Header(CLAY_STRING("Left Panel"), 2, {
.textColor = style::color::white
});
}
Clay_RenderCommandArray RenderApplication() {
Clay_BeginLayout();
CLAY(CLAY_ID("OuterContainer"), style::Window()) {
CLAY_AUTO_ID(style::PanelContainer(0, {
.layout = { .sizing = { CLAY_SIZING_PERCENT(0.15), CLAY_SIZING_GROW() } }
})) {
SampleHeader();
}
}
return Clay_EndLayout();
}
void HandleEvent(SDL_Event event) {
}
}

12
src/application.h Normal file
View file

@ -0,0 +1,12 @@
#ifndef APPLICATION_H
#define APPLICATION_H
#include <clay/clay.h>
#include <SDL3/SDL_events.h>
namespace application {
Clay_RenderCommandArray RenderApplication();
void HandleEvent(SDL_Event event);
}
#endif // !APPLICATION_H

84
src/elements.cpp Normal file
View file

@ -0,0 +1,84 @@
#include "elements.h"
#include "style.h"
#include "resources.h"
namespace elements {
void TextButton(Clay_String text, Clay_Color color, OnHoveredFn onHovered, intptr_t onHoveredData) {
Clay_Color hovered{ style::ToHoveredColor(color) };
CLAY_AUTO_ID({
.layout = {
.padding = style::buttonPadding,
.childAlignment = { CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = Clay_Hovered()
? hovered
: color,
.cornerRadius = style::buttonRadii,
.border = {
style::ToHoveredColor(Clay_Hovered() ? hovered : color),
CLAY_BORDER_ALL(2)
},
}) {
Clay_OnHover(onHovered, onHoveredData);
elements::Body(text, {
.textColor = style::TextColor(0),
.textAlignment = CLAY_TEXT_ALIGN_CENTER,
});
}
}
void ToggleHovered(Clay_ElementId element, Clay_PointerData pointer, intptr_t data) {
bool *hovered{ (bool*)data };
if (pointer.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
*hovered = !(*hovered);
}
}
void Toggle(Clay_String label, Clay_Color selected, bool &state) {
CLAY_AUTO_ID({
.layout = {
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() },
.childGap = 10,
.childAlignment = { CLAY_ALIGN_X_LEFT, CLAY_ALIGN_Y_CENTER },
},
}) {
Clay_Color color{Clay_Hovered()
? style::ToHoveredColor(selected)
: selected
};
Clay_OnHover(&ToggleHovered, (intptr_t)(&state));
CLAY_AUTO_ID({
.layout = {
.sizing = {
CLAY_SIZING_FIXED(20), CLAY_SIZING_FIXED(20)
}
},
.backgroundColor = (state
? selected
: style::color::transparent
),
.cornerRadius = style::buttonRadii,
.border = {
color,
CLAY_BORDER_OUTSIDE(3)
},
}) { }
Body(label, {
.textColor = style::TextColor(0)
});
}
}
void Body(Clay_String string, Clay_TextElementConfig baseCfg) {
baseCfg.fontId = resources::FONT_DEFAULT;
baseCfg.fontSize = style::baseFontSize;
CLAY_TEXT(string, CLAY_TEXT_CONFIG(baseCfg));
}
void Header(Clay_String string, size_t header, Clay_TextElementConfig baseCfg) {
baseCfg.fontId = resources::FONT_BOLD;
baseCfg.fontSize = style::headerSizes[(header)-1];
CLAY_TEXT(string, CLAY_TEXT_CONFIG(baseCfg));
}
}

14
src/elements.h Normal file
View file

@ -0,0 +1,14 @@
#ifndef ELEMENTS_H
#define ELEMENTS_H
#include <clay/clay.h>
namespace elements {
typedef void(*OnHoveredFn)(Clay_ElementId element, Clay_PointerData pointer, intptr_t data);
void TextButton(Clay_String text, Clay_Color color, OnHoveredFn onHovered, intptr_t onHoveredData = 0);
void Toggle(Clay_String label, Clay_Color selected, bool &state);
void Body(Clay_String string, Clay_TextElementConfig baseCfg = {});
void Header(Clay_String string, size_t header, Clay_TextElementConfig baseCfg = {});
}
#endif // !ELEMENTS_H

39
src/input.cpp Normal file
View file

@ -0,0 +1,39 @@
#include "input.h"
namespace input {
Clay_Vector2 scrollMotion{ 0, 0 };
bool shiftDown{ false };
bool mouseButtonDown{ false };
void FrameStart() {
scrollMotion = { 0, 0 };
}
void HandleEvent(SDL_Event const &event) {
switch (event.type) {
case SDL_EVENT_MOUSE_WHEEL:
if (shiftDown) {
scrollMotion = (Clay_Vector2) { event.wheel.y * 4.f, -event.wheel.x * -4.f };
} else {
scrollMotion = (Clay_Vector2) { -event.wheel.x * -4.f, event.wheel.y * 4.f };
}
break;
case SDL_EVENT_MOUSE_MOTION:
Clay_SetPointerState((Clay_Vector2) { event.motion.x, event.motion.y }, mouseButtonDown);
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
if (event.button.button == SDL_BUTTON_LEFT) {
mouseButtonDown = event.button.down;
Clay_SetPointerState((Clay_Vector2) { event.button.x, event.button.y }, mouseButtonDown);
}
break;
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
if (event.key.key == SDLK_LSHIFT || event.key.key == SDLK_RSHIFT) {
shiftDown = event.key.down;
}
break;
}
}
}

15
src/input.h Normal file
View file

@ -0,0 +1,15 @@
#ifndef INPUT_H
#define INPUT_H
#include <SDL3/SDL_events.h>
#include <clay/clay.h>
namespace input {
extern Clay_Vector2 scrollMotion;
extern bool shiftDown;
extern bool mouseButtonDown;
void FrameStart();
void HandleEvent(SDL_Event const &event);
}
#endif // !INPUT_H

144
src/main.cpp Normal file
View file

@ -0,0 +1,144 @@
#define SDL_MAIN_HANDLED
#include "application.h"
#include "input.h"
#include "resources.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_hints.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_keycode.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_render.h>
#include <SDL3/SDL_video.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <clay/clay.h>
#include <renderer/clay_renderer_SDL3.h>
#include <renderer/ui_data.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
constexpr SDL_InitFlags sdlInitFlags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY;
SDL_Window *window = nullptr;
SDL_Renderer *renderer = nullptr;
int screenWidth = 1920, screenHeight = 1080;
bool running = true;
uint64_t clayMemorySize = 0;
Clay_Arena clayPrimaryArena;
Clay_SDL3RendererData backendData = {
nullptr, nullptr, nullptr
};
static
Clay_Dimensions MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
TTF_Font **fonts = (TTF_Font**)userData;
TTF_Font *font = fonts[config->fontId];
int width, height;
TTF_SetFontSize(font, config->fontSize);
if (!TTF_GetStringSize(font, text.chars, text.length, &width, &height)) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "MeasureText failed to measure text %s", SDL_GetError());
}
return (Clay_Dimensions) { (float)width, (float)height };
}
static
void HandleClayErrors(Clay_ErrorData data) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "%s", data.errorText.chars);
}
static inline
void LogOutputResolution() {
int w, h;
SDL_GetCurrentRenderOutputSize(renderer, &w, &h);
SDL_Log("output size: %i, %d", w, h);
}
static inline
void InitSDL() {
SDL_SetHint(SDL_HINT_RENDER_LINE_METHOD, "3");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "SDL_Init failed: %s", SDL_GetError());
exit(1);
}
if ((window = SDL_CreateWindow("Window", screenWidth, screenHeight, sdlInitFlags)) == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "SDL_CreateWindow failed: %s", SDL_GetError());
exit(2);
}
if ((renderer = SDL_CreateRenderer(window, NULL)) == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "SDL_CreateRenderer failed: %s", SDL_GetError());
exit(3);
}
if (!TTF_Init()) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "TTF_Init failed: %s", SDL_GetError());
exit(4);
}
if ((resources::textEngine = TTF_CreateRendererTextEngine(renderer)) == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "TTF_CreateRendererTextEngine failed: %s", SDL_GetError());
exit(5);
}
}
static
void InitClay() {
clayMemorySize = Clay_MinMemorySize();
clayPrimaryArena = Clay_CreateArenaWithCapacityAndMemory(clayMemorySize, SDL_malloc(clayMemorySize));
Clay_Initialize(clayPrimaryArena, { (float)screenWidth, (float)screenHeight }, { HandleClayErrors });
Clay_SetMeasureTextFunction(MeasureText, resources::fonts);
Clay_SetLayoutDimensions({ (float)screenWidth, (float)screenHeight });
float x{ 0 }, y{ 0 };
SDL_GetMouseState(&x, &y);
Clay_SetPointerState((Clay_Vector2) { x, y }, false);
}
int main(int argc, char *argv[]) {
InitSDL();
resources::LoadResources();
LogOutputResolution();
InitClay();
backendData = { renderer, resources::textEngine, resources::fonts };
SDL_Event event;
uint64_t startFrameTime = SDL_GetTicksNS();
double deltaTime = 0.0;
while (running) {
std::srand(SDL_GetTicksNS());
deltaTime = SDL_GetTicksNS() - startFrameTime;
startFrameTime = SDL_GetTicksNS();
UiData_Clear();
input::FrameStart();
while (SDL_PollEvent(&event)) {
application::HandleEvent(event);
input::HandleEvent(event);
switch (event.type) {
case SDL_EVENT_QUIT:
running = false;
break;
case SDL_EVENT_WINDOW_RESIZED:
Clay_SetLayoutDimensions({
(float)event.window.data1,
(float)event.window.data2
});
LogOutputResolution();
break;
default: break;
}
}
Clay_UpdateScrollContainers(true, input::scrollMotion, deltaTime);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
Clay_RenderCommandArray commands{ application::RenderApplication() };
SDL_Clay_RenderClayCommands(&backendData, &commands);
SDL_RenderPresent(renderer);
SDL_Delay(10);
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}

32
src/resources.cpp Normal file
View file

@ -0,0 +1,32 @@
#include "resources.h"
#include "style.h"
#include <clay/clay.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_render.h>
#include <SDL3_image/SDL_image.h>
namespace resources {
TTF_Font *fonts[FONT_MAX];
TTF_TextEngine *textEngine = nullptr;
static inline void LoadFonts() {
fonts[FONT_DEFAULT] = TTF_OpenFont("assets/AdwaitaSans-Regular.ttf", style::baseFontSize * 5);
if (fonts[FONT_DEFAULT] == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "TTF_OpenFont failed: Failed to load adwaita sans: %s", SDL_GetError());
exit(6);
}
TTF_SetFontHinting(fonts[FONT_DEFAULT], TTF_HINTING_LIGHT_SUBPIXEL);
fonts[FONT_BOLD] = TTF_OpenFont("assets/AdwaitaSans-Regular.ttf", style::baseFontSize * 5);
if (fonts[FONT_BOLD] == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "TTF_OpenFont failed: Failed to load adwaita sans bold: %s", SDL_GetError());
exit(6);
}
TTF_SetFontHinting(fonts[FONT_BOLD], TTF_HINTING_LIGHT_SUBPIXEL);
TTF_SetFontStyle(fonts[FONT_BOLD], TTF_STYLE_BOLD);
SDL_Log("LoadFonts: Success");
}
void LoadResources() {
LoadFonts();
}
}

19
src/resources.h Normal file
View file

@ -0,0 +1,19 @@
#ifndef RESOURCES_H
#define RESOURCES_H
#include <SDL3_ttf/SDL_ttf.h>
namespace resources {
enum Font {
FONT_DEFAULT = 0,
FONT_BOLD = 1,
FONT_MAX
};
extern TTF_TextEngine *textEngine;
extern TTF_Font *fonts[FONT_MAX];
void LoadResources();
}
#endif // !RESOURCES_H

87
src/style.cpp Normal file
View file

@ -0,0 +1,87 @@
#include "style.h"
#include "resources.h"
#include <clay/clay.h>
namespace style {
Clay_ElementDeclaration ListContainer(size_t depth, Clay_ElementDeclaration baseCfg) {
baseCfg.border = {
PanelBorder(depth),
CLAY_BORDER_ALL(2)
};
baseCfg.cornerRadius = defaultRadiusAll;
return baseCfg;
}
Clay_ElementDeclaration PanelContainer(size_t depth, Clay_ElementDeclaration baseCfg) {
baseCfg.backgroundColor = PanelBackground(depth);
baseCfg.border = {
PanelBorder(depth),
CLAY_BORDER_OUTSIDE(2)
};
baseCfg.cornerRadius = defaultRadiusAll;
baseCfg.layout.padding = CLAY_PADDING_ALL(8);
return baseCfg;
}
Clay_ElementDeclaration LeftPanelContainer(size_t depth, Clay_ElementDeclaration baseCfg) {
baseCfg = PanelContainer(depth, baseCfg);
baseCfg.border.width = { 0, 2, 2, 2, 0 };
baseCfg.cornerRadius = { 0, defaultRadius, 0, defaultRadius };
baseCfg.layout.sizing.height = CLAY_SIZING_GROW();
return baseCfg;
}
Clay_ElementDeclaration RightPanelContainer(size_t depth, Clay_ElementDeclaration baseCfg) {
baseCfg = PanelContainer(depth, baseCfg);
baseCfg.border.width = { 2, 0, 2, 2, 0 };
baseCfg.cornerRadius = { defaultRadius, 0, defaultRadius, 0 };
baseCfg.layout.sizing.height = CLAY_SIZING_GROW();
return baseCfg;
}
Clay_Color PanelBackground(size_t idx) {
return {
255*panelBackground[idx],
255*panelBackground[idx],
255*panelBackground[idx],
255
};
}
Clay_Color PanelBorder(size_t idx) {
return {
255*panelBorder[idx],
255*panelBorder[idx],
255*panelBorder[idx],
255
};
}
Clay_Color TextColor(size_t idx) {
return {
255*textColorsP[idx],
255*textColorsP[idx],
255*textColorsP[idx],
255
};
}
Clay_ElementDeclaration Window() {
return {
.layout = {
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() },
.padding = CLAY_PADDING_ALL(0),
.childGap = 0,
.layoutDirection = CLAY_LEFT_TO_RIGHT,
},
};
}
Clay_Color ToHoveredColor(Clay_Color color) {
float avg = (color.r + color.g + color.b) / 3.f;
color.r = (color.r - avg) * 0.8f + avg - 30;
color.g = (color.g - avg) * 0.8f + avg - 30;
color.b = (color.b - avg) * 0.8f + avg - 30;
return color;
}
}

104
src/style.h Normal file
View file

@ -0,0 +1,104 @@
#ifndef STYLE_H
#define STYLE_H
#include <clay/clay.h>
#include <stdint.h>
namespace style {
////////////////////////////////////
// WINDOW STYLE
////////////////////////////////////
constexpr uint16_t windowPadding{ 10 };
////////////////////////////////////
// CONTAINER STYLE
////////////////////////////////////
constexpr uint16_t containerGap{ 10 };
constexpr double defaultRadius{ 5.0 };
constexpr float panelBackground[] = {
.4f, .3f, .2f
};
constexpr float panelBorder[] = {
.5f, .4f, .3f
};
constexpr Clay_Padding panelPadding = {
24, 24,
24, 24,
};
Clay_ElementDeclaration ListContainer(size_t depth, Clay_ElementDeclaration baseCfg);
Clay_ElementDeclaration PanelContainer(size_t depth, Clay_ElementDeclaration baseCfg);
Clay_ElementDeclaration LeftPanelContainer(size_t depth, Clay_ElementDeclaration baseCfg);
Clay_ElementDeclaration RightPanelContainer(size_t depth, Clay_ElementDeclaration baseCfg);
Clay_Color PanelBackground(size_t idx);
Clay_Color PanelBorder(size_t idx);
Clay_ElementDeclaration Window();
////////////////////////////////////
// TEXT STYLE
////////////////////////////////////
constexpr float paragraphGap = 10;
constexpr uint16_t baseFontSize = 16;
constexpr float textColorsP[] = {
0.9f, 0.9f, 0.9f
};
constexpr uint16_t headerSizes[] = {
64, 32,
28, 16
};
Clay_Color TextColor(size_t idx);
////////////////////////////////////
// BUTTONS
////////////////////////////////////
constexpr Clay_Color warningButton = {
177, 56, 52, 255
};
constexpr Clay_Color proceedButton = {
49, 181, 99, 255
};
constexpr Clay_Color actionButton = {
49, 142, 181, 255
};
constexpr Clay_Padding buttonPadding = {
24, 24,
4, 4,
};
constexpr Clay_CornerRadius buttonRadii = {
3, 3, 3, 3
};
Clay_Color ToHoveredColor(Clay_Color color);
////////////////////////////////////
// COMPILATIONS
// | Functions and expressions that combine styling data from the settings above.
////////////////////////////////////
constexpr Clay_Sizing layoutExpand = {
.width = CLAY_SIZING_GROW(0),
.height = CLAY_SIZING_GROW(0)
};
constexpr Clay_CornerRadius defaultRadiusAll = {
defaultRadius, defaultRadius,
defaultRadius, defaultRadius
};
namespace color {
constexpr Clay_Color transparent{ 255, 255, 255, 0 };
constexpr Clay_Color black{ 0, 0, 0, 255 };
constexpr Clay_Color white{ 255, 255, 255, 255 };
}
}
#endif // !STYLE_H

54
src/thread_pool.cpp Normal file
View file

@ -0,0 +1,54 @@
#include "thread_pool.h"
#include <algorithm>
#include <thread>
#include <utility>
namespace threading {
ThreadPool::ThreadPool() {
size_t const thread_count{ std::max(std::thread::hardware_concurrency() - 1, 1u) };
this->threads.reserve(thread_count);
for (size_t i{ 0 }; i < thread_count; ++i) {
this->threads.emplace_back(std::bind(&ThreadPool::ThreadFn, this));
}
}
ThreadPool::~ThreadPool() {
{ std::scoped_lock lock{ this->lock };
this->shutdown = true;
this->threadNotifier.notify_all();
}
for (std::thread &thread : this->threads) {
thread.join();
}
}
size_t ThreadPool::GetThreadCount() {
return this->threads.size();
}
void ThreadPool::ScheduleTask(TaskFunc fn) {
std::scoped_lock lock{ this->lock };
this->taskQueue.emplace(std::move(fn));
this->threadNotifier.notify_one();
}
void ThreadPool::ThreadFn() {
TaskFunc function;
for(;;) {
{ std::unique_lock<std::mutex> lock{ this->lock };
while (!this->shutdown && this->taskQueue.empty()) {
this->threadNotifier.wait(lock);
}
if (this->taskQueue.empty()) {
return;
}
function = std::move(this->taskQueue.front());
this->taskQueue.pop();
}
function.operator()();
}
}
ThreadPool tasks{};
}

30
src/thread_pool.h Normal file
View file

@ -0,0 +1,30 @@
#ifndef THREAD_POOL_H
#define THREAD_POOL_H
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
namespace threading {
typedef std::function<void()> TaskFunc;
class ThreadPool {
public: ThreadPool();
~ThreadPool();
void ScheduleTask(TaskFunc jobs);
size_t GetThreadCount();
private:
void ThreadFn();
std::queue<TaskFunc> taskQueue{};
std::vector<std::thread> threads{};
std::mutex lock{};
std::condition_variable threadNotifier{};
bool shutdown{ false };
};
extern ThreadPool tasks;
}
#endif // !THREAD_POOL_H

1
vendor/SDL3 vendored Submodule

@ -0,0 +1 @@
Subproject commit 137b0b2bee2bb9dd76e2c260970ccf7ff0621692

1
vendor/SDL3_ttf vendored Submodule

@ -0,0 +1 @@
Subproject commit 6b6bd588e8646360b08f624fb601cc2ec75c6ada

4446
vendor/clay/clay.h vendored Normal file

File diff suppressed because it is too large Load diff

259
vendor/renderer/clay_renderer_SDL3.c vendored Normal file
View file

@ -0,0 +1,259 @@
#include "clay_renderer_SDL3.h"
#define CLAY_IMPLEMENTATION
#include <clay/clay.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_render.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <assert.h>
#include <stddef.h>
/* Global for convenience. Even in 4K this is enough for smooth curves (low radius or rect size coupled with
* no AA or low resolution might make it appear as jagged curves) */
static int NUM_CIRCLE_SEGMENTS = 16;
static void SDL_Clay_GenerateRoundedRectCorner(SDL_Vertex *vertices, size_t begin, size_t count, SDL_FPoint origin, SDL_FPoint offset, SDL_FColor color, SDL_FPoint texel, float radius) {
assert(count > 0);
if (count == 1) {
vertices[begin] = (SDL_Vertex){ { origin.x, origin.y }, color, texel };
} else {
const double quarterCircleRadians = SDL_PI_D / 2.0;
for (size_t i = 0; i < count; ++i) {
double const angle = (quarterCircleRadians / ((double)count-1)) * i;
vertices[begin + i] = (SDL_Vertex) { .position = {
origin.x + (radius * (offset.x * SDL_cos(angle) - offset.y * SDL_sin(angle))),
origin.y + (radius * (offset.x * SDL_sin(angle) + offset.y * SDL_cos(angle)))
},
.color = color, .tex_coord = texel
};
}
}
}
//all rendering is performed by a single SDL call, avoiding multiple RenderRect + plumbing choice for circles.
static void SDL_Clay_RenderFillRoundedRect(Clay_SDL3RendererData *rendererData, const SDL_FRect rect, const Clay_CornerRadius cornerRadius, const Clay_Color clayColor) {
static const SDL_FPoint topLeftTexel = { 0, 0 };
static const SDL_FPoint topRightTexel = { 1, 0 };
static const SDL_FPoint bottomRightTexel = { 1, 1 };
static const SDL_FPoint bottomLeftTexel = { 0, 1 };
SDL_FColor const color = { clayColor.r/255, clayColor.g/255, clayColor.b/255, clayColor.a/255 };
const float maxRadius = SDL_min(rect.w, rect.h) / 2.0f;
float radii[4] = {
SDL_min(cornerRadius.topLeft, maxRadius),
SDL_min(cornerRadius.topRight, maxRadius),
SDL_min(cornerRadius.bottomLeft, maxRadius),
SDL_min(cornerRadius.bottomRight, maxRadius),
};
int numCircleSegments[4] = { 1, 1, 1, 1 };
int totalVertices = 0;
for (int i = 0; i < 4; ++i) {
numCircleSegments[i] = SDL_clamp((int)radii[i], 1, NUM_CIRCLE_SEGMENTS);
totalVertices += numCircleSegments[i];
}
size_t vindex = 0;
// generate topleft corner
SDL_FPoint origin = {
rect.x + radii[0],
rect.y + radii[0]
};
SDL_FPoint offset = { -1, 0 };
SDL_Vertex vertices[totalVertices];
SDL_Clay_GenerateRoundedRectCorner(vertices, vindex, numCircleSegments[0], origin, offset, color, topLeftTexel, radii[0]);
vindex += numCircleSegments[0];
origin = (SDL_FPoint) {
rect.x + rect.w - radii[1],
rect.y + radii[1]
};
offset = (SDL_FPoint) { 0, -1 };
SDL_Clay_GenerateRoundedRectCorner(vertices, vindex, numCircleSegments[1], origin, offset, color, topRightTexel, radii[1]);
vindex += numCircleSegments[1];
origin = (SDL_FPoint) {
rect.x + rect.w - radii[2],
rect.y + rect.h - radii[2]
};
offset = (SDL_FPoint) { 1, 0 };
SDL_Clay_GenerateRoundedRectCorner(vertices, vindex, numCircleSegments[2], origin, offset, color, bottomRightTexel, radii[2]);
vindex += numCircleSegments[2];
origin = (SDL_FPoint) {
rect.x + radii[3],
rect.y + rect.h - radii[3]
};
offset = (SDL_FPoint) { 0, 1 };
SDL_Clay_GenerateRoundedRectCorner(vertices, vindex, numCircleSegments[3], origin, offset, color, bottomLeftTexel, radii[3]);
vindex += numCircleSegments[3];
assert(vindex == totalVertices);
size_t totalTris = totalVertices - 2;
int totalIndeces = totalTris * 3;
int indeces[totalTris * 3];
for (size_t i = 0; i < totalTris; ++i) {
int *tri = indeces + (i * 3);
tri[0] = i + 1;
tri[1] = i + 2;
tri[2] = 0;
}
// Render everything
SDL_RenderGeometry(rendererData->renderer, NULL, vertices, totalVertices, indeces, totalIndeces);
#if 0
// Render debug wireframe
for (size_t i = 0; i < totalTris; ++i) {
SDL_SetRenderDrawColor(rendererData->renderer, 255, 255, 255, 255);
SDL_RenderLine(rendererData->renderer, vertices[0].position.x, vertices[0].position.y, vertices[i+2].position.x, vertices[i+2].position.y);
SDL_RenderLine(rendererData->renderer, vertices[0].position.x, vertices[0].position.y, vertices[i+1].position.x, vertices[i+1].position.y);
SDL_RenderLine(rendererData->renderer, vertices[i+1].position.x, vertices[i+1].position.y, vertices[i+2].position.x, vertices[i+2].position.y);
}
#endif
}
static void SDL_Clay_RenderArc(Clay_SDL3RendererData *rendererData, const SDL_FPoint center, const float radius, const float startAngle, const float endAngle, const float thickness, const Clay_Color color) {
SDL_SetRenderDrawColor(rendererData->renderer, color.r, color.g, color.b, color.a);
const float radStart = startAngle * (SDL_PI_F / 180.0f);
const float radEnd = endAngle * (SDL_PI_F / 180.0f);
const int numCircleSegments = SDL_max(NUM_CIRCLE_SEGMENTS, (int)(radius * 1.5f)); //increase circle segments for larger circles, 1.5 is arbitrary.
const float angleStep = (radEnd - radStart) / (float)numCircleSegments;
const float thicknessStep = 0.4f; //arbitrary value to avoid overlapping lines. Changing THICKNESS_STEP or numCircleSegments might cause artifacts.
for (float t = thicknessStep; t < thickness - thicknessStep; t += thicknessStep) {
SDL_FPoint points[numCircleSegments + 1];
const float clampedRadius = SDL_max(radius - t, 1.0f);
for (int i = 0; i <= numCircleSegments; i++) {
const float angle = radStart + i * angleStep;
points[i] = (SDL_FPoint){
SDL_roundf(center.x + SDL_cosf(angle) * clampedRadius),
SDL_roundf(center.y + SDL_sinf(angle) * clampedRadius) };
}
SDL_RenderLines(rendererData->renderer, points, numCircleSegments + 1);
}
}
SDL_Rect currentClippingRectangle;
void SDL_Clay_RenderClayCommands(Clay_SDL3RendererData *rendererData, Clay_RenderCommandArray *rcommands)
{
for (size_t i = 0; i < rcommands->length; i++) {
Clay_RenderCommand *rcmd = Clay_RenderCommandArray_Get(rcommands, i);
const Clay_BoundingBox bounding_box = rcmd->boundingBox;
const SDL_FRect rect = { (int)bounding_box.x, (int)bounding_box.y, (int)bounding_box.width, (int)bounding_box.height };
switch (rcmd->commandType) {
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
Clay_RectangleRenderData *config = &rcmd->renderData.rectangle;
SDL_SetRenderDrawBlendMode(rendererData->renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(rendererData->renderer, config->backgroundColor.r, config->backgroundColor.g, config->backgroundColor.b, config->backgroundColor.a);
if (config->cornerRadius.topLeft > 0 || config->cornerRadius.topRight > 0 || config->cornerRadius.bottomLeft > 0 || config->cornerRadius.bottomRight > 0) {
SDL_Clay_RenderFillRoundedRect(rendererData, rect, config->cornerRadius, config->backgroundColor);
} else {
SDL_RenderFillRect(rendererData->renderer, &rect);
}
} break;
case CLAY_RENDER_COMMAND_TYPE_TEXT: {
Clay_TextRenderData *config = &rcmd->renderData.text;
TTF_Font *font = rendererData->fonts[config->fontId];
TTF_SetFontSize(font, config->fontSize);
TTF_Text *text = TTF_CreateText(rendererData->textEngine, font, config->stringContents.chars, config->stringContents.length);
TTF_SetTextColor(text, config->textColor.r, config->textColor.g, config->textColor.b, config->textColor.a);
TTF_DrawRendererText(text, rect.x, rect.y);
TTF_DestroyText(text);
} break;
case CLAY_RENDER_COMMAND_TYPE_BORDER: {
Clay_BorderRenderData *config = &rcmd->renderData.border;
const float minRadius = SDL_min(rect.w, rect.h) / 2.0f;
const Clay_CornerRadius clampedRadii = {
.topLeft = SDL_min(config->cornerRadius.topLeft, minRadius),
.topRight = SDL_min(config->cornerRadius.topRight, minRadius),
.bottomLeft = SDL_min(config->cornerRadius.bottomLeft, minRadius),
.bottomRight = SDL_min(config->cornerRadius.bottomRight, minRadius)
};
//edges
SDL_SetRenderDrawColor(rendererData->renderer, config->color.r, config->color.g, config->color.b, config->color.a);
if (config->width.left > 0) {
const float starting_y = rect.y + clampedRadii.topLeft;
const float length = rect.h - clampedRadii.topLeft - clampedRadii.bottomLeft;
SDL_FRect line = { rect.x - 1, starting_y, config->width.left, length };
SDL_RenderFillRect(rendererData->renderer, &line);
}
if (config->width.right > 0) {
const float starting_x = rect.x + rect.w - (float)config->width.right + 1;
const float starting_y = rect.y + clampedRadii.topRight;
const float length = rect.h - clampedRadii.topRight - clampedRadii.bottomRight;
SDL_FRect line = { starting_x, starting_y, config->width.right, length };
SDL_RenderFillRect(rendererData->renderer, &line);
}
if (config->width.top > 0) {
const float starting_x = rect.x + clampedRadii.topLeft;
const float length = rect.w - clampedRadii.topLeft - clampedRadii.topRight;
SDL_FRect line = { starting_x, rect.y - 1, length, config->width.top };
SDL_RenderFillRect(rendererData->renderer, &line);
}
if (config->width.bottom > 0) {
const float starting_x = rect.x + clampedRadii.bottomLeft;
const float starting_y = rect.y + rect.h - (float)config->width.bottom + 1;
const float length = rect.w - clampedRadii.bottomLeft - clampedRadii.bottomRight;
SDL_FRect line = { starting_x, starting_y, length, config->width.bottom };
SDL_SetRenderDrawColor(rendererData->renderer, config->color.r, config->color.g, config->color.b, config->color.a);
SDL_RenderFillRect(rendererData->renderer, &line);
}
//corners
if (config->cornerRadius.topLeft > 0) {
const float centerX = rect.x + clampedRadii.topLeft -1;
const float centerY = rect.y + clampedRadii.topLeft - 1;
SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.topLeft,
180.0f, 270.0f, config->width.top, config->color);
}
if (config->cornerRadius.topRight > 0) {
const float centerX = rect.x + rect.w - clampedRadii.topRight;
const float centerY = rect.y + clampedRadii.topRight - 1;
SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.topRight,
270.0f, 360.0f, config->width.top, config->color);
}
if (config->cornerRadius.bottomLeft > 0) {
const float centerX = rect.x + clampedRadii.bottomLeft -1;
const float centerY = rect.y + rect.h - clampedRadii.bottomLeft;
SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.bottomLeft,
90.0f, 180.0f, config->width.bottom, config->color);
}
if (config->cornerRadius.bottomRight > 0) {
const float centerX = rect.x + rect.w - clampedRadii.bottomRight;
const float centerY = rect.y + rect.h - clampedRadii.bottomRight;
SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.bottomRight,
0.0f, 90.0f, config->width.bottom, config->color);
}
} break;
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
Clay_BoundingBox boundingBox = rcmd->boundingBox;
currentClippingRectangle = (SDL_Rect) {
.x = boundingBox.x,
.y = boundingBox.y,
.w = boundingBox.width,
.h = boundingBox.height,
};
SDL_SetRenderClipRect(rendererData->renderer, &currentClippingRectangle);
break;
}
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
SDL_SetRenderClipRect(rendererData->renderer, NULL);
break;
}
case CLAY_RENDER_COMMAND_TYPE_IMAGE: {
SDL_Texture *texture = (SDL_Texture *)rcmd->renderData.image.imageData;
const SDL_FRect dest = { rect.x, rect.y, rect.w, rect.h };
SDL_RenderTexture(rendererData->renderer, texture, NULL, &dest);
break;
}
default:
SDL_Log("Unknown render command type: %d", rcmd->commandType);
}
}
}

22
vendor/renderer/clay_renderer_SDL3.h vendored Normal file
View file

@ -0,0 +1,22 @@
#ifndef CLAY_RENDERER_SDL3_H
#define CLAY_RENDERER_SDL3_H
#ifdef __cplusplus
extern "C" {
#endif
#include <clay/clay.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <SDL3/SDL_render.h>
typedef struct {
SDL_Renderer *renderer;
TTF_TextEngine *textEngine;
TTF_Font **fonts;
} Clay_SDL3RendererData;
extern void SDL_Clay_RenderClayCommands(Clay_SDL3RendererData *rendererData, Clay_RenderCommandArray *rcommands);
#ifdef __cplusplus
}
#endif
#endif // !CLAY_RENDERER_SDL3_H

30
vendor/renderer/ui_data.c vendored Normal file
View file

@ -0,0 +1,30 @@
#include "ui_data.h"
#include <stdint.h>
#include <string.h>
const size_t uiDataLength = UI_DATA_SIZE;
static size_t utilized = 9;
static char uiDataArena[UI_DATA_SIZE];
static char *uiDataWriter = uiDataArena;
Clay_String UiData_StoreString(char const *data, size_t amount) {
if (utilized + amount > uiDataLength) {
return CLAY_STRING("out of arena memory");
}
memcpy(uiDataWriter, data, amount);
Clay_String result = {
false, (int32_t)amount, uiDataWriter
};
uiDataWriter += amount;
utilized += amount;
return result;
}
Clay_String UiData_StoreClayStr(Clay_String string) {
return UiData_StoreString(string.chars, string.length);
}
void UiData_Clear() {
utilized = 0;
uiDataWriter = uiDataArena;
}

23
vendor/renderer/ui_data.h vendored Normal file
View file

@ -0,0 +1,23 @@
#ifndef UI_DATA_H
#define UI_DATA_H
#include <clay/clay.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef UI_DATA_SIZE
#define UI_DATA_SIZE 1024*1024
#endif
extern Clay_String UiData_StoreString(char const *data, size_t amount);
extern Clay_String UiData_StoreClayStr(Clay_String str);
extern void UiData_Clear();
#ifdef __cplusplus
}
#endif
#endif // !UI_DATA_