diff --git a/.gitignore b/.gitignore index b3395e4..a8f4b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build-wasm/ compile_commands.json /.emscripten-cache /.cache/clangd/index +/build-asan diff --git a/CMakeLists.txt b/CMakeLists.txt index 5115aac..36851fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,11 +5,19 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(REGOLITH_BUILD_TESTS "Build tests" ON) option(REGOLITH_BUILD_SDL3 "Build the SDL3 backend" ON) +option(REGOLITH_SANITIZE "Build with ASan and UBSan" OFF) +if(REGOLITH_SANITIZE AND NOT MSVC AND NOT EMSCRIPTEN) + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) +endif() add_subdirectory(third_party) add_library(regolith src/context.c + src/nodes.c + src/props.c + src/types.c src/layout_clay.c src/text_stb.c src/lua_module.c diff --git a/include/regolith.h b/include/regolith.h index 643b916..e8c4e77 100644 --- a/include/regolith.h +++ b/include/regolith.h @@ -39,7 +39,10 @@ enum { RG_ERR_GENERIC = -1, RG_ERR_NO_MEMORY = -2, RG_ERR_STATE = -3, - RG_ERR_LUA = -4 + RG_ERR_LUA = -4, + RG_ERR_HANDLE = -5, + RG_ERR_TYPE = -6, + RG_ERR_PROP = -7 }; enum { RG_LOG_TRACE = 0, RG_LOG_DEBUG, RG_LOG_INFO, RG_LOG_WARN, RG_LOG_ERROR }; @@ -72,6 +75,96 @@ RG_API rg_context *rg_create(const rg_config *config); RG_API void rg_destroy(rg_context *ctx); RG_API const char *rg_last_error(const rg_context *ctx); +typedef uint64_t rg_node; +#define RG_NODE_NONE ((rg_node)0) + +enum { + RG_NODE_ROOT = 0, + RG_NODE_BOX, + RG_NODE_HBOX, + RG_NODE_VBOX, + RG_NODE_PANEL, + RG_NODE_LABEL, + RG_NODE_BUTTON, + RG_NODE_TYPE_COUNT +}; + +enum { + RG_VAL_NONE = 0, + RG_VAL_BOOL, + RG_VAL_INT, + RG_VAL_FLOAT, + RG_VAL_ENUM, + RG_VAL_COLOR, + RG_VAL_SIZE, + RG_VAL_VEC4, + RG_VAL_STRING +}; + +enum { RG_SIZE_FIT = 0, RG_SIZE_GROW, RG_SIZE_FIXED, RG_SIZE_PERCENT }; +enum { RG_DIR_ROW = 0, RG_DIR_COLUMN }; +enum { RG_ALIGN_START = 0, RG_ALIGN_CENTER, RG_ALIGN_END }; + +enum { + RG_PROP_DIRECTION = 0, + RG_PROP_WIDTH, + RG_PROP_HEIGHT, + RG_PROP_PADDING, + RG_PROP_GAP, + RG_PROP_ALIGN_X, + RG_PROP_ALIGN_Y, + RG_PROP_BACKGROUND, + RG_PROP_BORDER_COLOR, + RG_PROP_BORDER_WIDTH, + RG_PROP_CORNER_RADIUS, + RG_PROP_OPACITY, + RG_PROP_VISIBLE, + RG_PROP_TEXT, + RG_PROP_FONT, + RG_PROP_FONT_SIZE, + RG_PROP_COLOR, + RG_PROP_COUNT +}; + +typedef struct rg_value { + int32_t type; + int32_t i; + float f[4]; + const char *s; +} rg_value; + +RG_API int32_t rg_type_find(const char *name); +RG_API const char *rg_type_name(int32_t type); + +RG_API rg_node rg_root(const rg_context *ctx); +RG_API rg_node rg_node_create(rg_context *ctx, int32_t type); +RG_API void rg_node_destroy(rg_context *ctx, rg_node node); +RG_API int rg_node_valid(const rg_context *ctx, rg_node node); +RG_API int32_t rg_node_type(const rg_context *ctx, rg_node node); +RG_API int rg_node_append(rg_context *ctx, rg_node parent, rg_node child); +RG_API int rg_node_insert_before(rg_context *ctx, rg_node parent, rg_node child, + rg_node ref); +RG_API int rg_node_detach(rg_context *ctx, rg_node node); +RG_API rg_node rg_node_parent(const rg_context *ctx, rg_node node); +RG_API rg_node rg_node_first_child(const rg_context *ctx, rg_node node); +RG_API rg_node rg_node_next_sibling(const rg_context *ctx, rg_node node); + +RG_API int32_t rg_prop_find(const char *name); +RG_API const char *rg_prop_name(int32_t prop); +RG_API int32_t rg_prop_type(int32_t prop); + +RG_API int rg_set(rg_context *ctx, rg_node node, int32_t prop, + const rg_value *value); +RG_API int rg_get(rg_context *ctx, rg_node node, int32_t prop, rg_value *out); +RG_API int rg_unset(rg_context *ctx, rg_node node, int32_t prop); + +RG_API int rg_set_int(rg_context *ctx, rg_node node, int32_t prop, int32_t v); +RG_API int rg_set_float(rg_context *ctx, rg_node node, int32_t prop, float v); +RG_API int rg_set_string(rg_context *ctx, rg_node node, int32_t prop, + const char *v); +RG_API int rg_set_color(rg_context *ctx, rg_node node, int32_t prop, float r, + float g, float b, float a); + #ifdef __cplusplus } #endif diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..005af99 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +usage() { + cat < + +commands: + debug configure, build, and test into build/ + release configure, build, and test into build-release/ + asan configure, build, and test with ASan and UBSan into build-asan/ + wasm configure, build, and test with Emscripten into build-wasm/ + all debug, asan, then wasm + clean remove all build directories +EOF +} + +build() { + local dir=$1 launcher=$2 + shift 2 + $launcher cmake -S . -B "$dir" -G Ninja "$@" + cmake --build "$dir" + ctest --test-dir "$dir" --output-on-failure +} + +link_compile_commands() { + ln -sf build/compile_commands.json compile_commands.json +} + +case "${1:-}" in + debug) + build build "" -DCMAKE_BUILD_TYPE=Debug + link_compile_commands + ;; + release) + build build-release "" -DCMAKE_BUILD_TYPE=Release + ;; + asan) + build build-asan "" -DCMAKE_BUILD_TYPE=Debug -DREGOLITH_SANITIZE=ON + ;; + wasm) + build build-wasm emcmake -DCMAKE_BUILD_TYPE=Debug + ;; + all) + "$0" debug + "$0" asan + "$0" wasm + ;; + clean) + rm -rf build build-release build-asan build-wasm compile_commands.json + ;; + *) + usage + exit 1 + ;; +esac diff --git a/src/context.c b/src/context.c index 20939cb..31b43d2 100644 --- a/src/context.c +++ b/src/context.c @@ -1,4 +1,5 @@ #include "internal.h" +#include "regolith.h" #include #include @@ -102,6 +103,11 @@ rg_context *rg_create(const rg_config *config) { ctx->viewport_h = cfg.viewport_h; ctx->display_scale = cfg.display_scale; + if(rg__nodes_init(ctx) != RG_OK) { + rg_destroy(ctx); + return NULL; + } + rg__log(ctx, RG_LOG_DEBUG, "clay minimum memory: %zu bytes", rg__clay_min_memory()); rg__log(ctx, RG_LOG_INFO, "context created (%dx%d @ %.2fx)", @@ -116,7 +122,31 @@ void rg_destroy(rg_context *ctx) { if (!ctx) return; rg__lua_close(ctx); + rg__nodes_free(ctx); rg__log(ctx, RG_LOG_INFO, "context destroyed"); a = ctx->alloc; a.free(ctx, sizeof *ctx, a.user); } + +void *rg__alloc(rg_context *ctx, size_t size) { + return ctx->alloc.alloc(size, ctx->alloc.user); +} + +void *rg__zalloc(rg_context *ctx, size_t size) { + void *p = rg__alloc(ctx, size); + if (p) + memset(p, 0, size); + return p; +} + +void *rg__realloc(rg_context *ctx, void *ptr, size_t old_size, + size_t new_size) { + if (!ptr) + return rg__alloc(ctx, new_size); + return ctx->alloc.realloc(ptr, old_size, new_size, ctx->alloc.user); +} + +void rg__free(rg_context *ctx, void *ptr, size_t size) { + if (ptr) + ctx->alloc.free(ptr, size, ctx->alloc.user); +} diff --git a/src/internal.h b/src/internal.h index 6306570..5b0c1c0 100644 --- a/src/internal.h +++ b/src/internal.h @@ -1,33 +1,99 @@ #ifndef RG_INTERNAL_H #define RG_INTERNAL_H - #include "regolith.h" #if defined(__GNUC__) - #define RG_PRINTF(f, a) __attribute__((format(printf, f, a))) +#define RG_PRINTF(f, a) __attribute__((format(printf, f, a))) #else - #define RG_PRINTF(f, a) +#define RG_PRINTF(f, a) #endif +#define RG_NIL 0xFFFFFFFFu + +enum { RG_PF_INHERIT = 1, RG_PF_LAYOUT = 2, RG_PF_ANIMATE = 4 }; +enum { RG_TF_FOCUSABLE = 1, RG_TF_TEXT = 2 }; + +#define RG_V_BOOL(v) {RG_VAL_BOOL, (v), {0, 0, 0, 0}, NULL} +#define RG_V_ENUM(v) {RG_VAL_ENUM, (v), {0, 0, 0, 0}, NULL} +#define RG_V_FLOAT(v) {RG_VAL_FLOAT, 0, {(v), 0, 0, 0}, NULL} +#define RG_V_COLOR(r, g, b, a) {RG_VAL_COLOR, 0, {(r), (g), (b), (a)}, NULL} +#define RG_V_VEC4(a, b, c, d) {RG_VAL_VEC4, 0, {(a), (b), (c), (d)}, NULL} +#define RG_V_SIZE(mode, v) {RG_VAL_SIZE, (mode), {(v), 0, 0, 0}, NULL} +#define RG_V_STRING(s) {RG_VAL_STRING, 0, {0, 0, 0, 0}, (s)} + +typedef struct rg_override { + int32_t prop; + rg_value value; +} rg_override; + +typedef struct rg_prop_info { + const char *name; + int32_t type; + uint32_t flags; + rg_value def; +} rg_prop_info; + +typedef struct rg_type_info { + const char *name; + uint32_t flags; + const rg_override *defaults; + int32_t default_count; +} rg_type_info; + +typedef struct rg_node_data { + uint32_t index; + uint32_t generation; + uint32_t alive; + uint32_t parent, first_child, last_child, prev, next; + uint32_t next_free; + int32_t type; + rg_override *ov; + uint32_t ov_count, ov_cap; +} rg_node_data; + struct lua_State; struct rg_context { rg_allocator alloc; rg_log_fn log; - void* log_user; + void *log_user; int32_t log_level; int32_t viewport_w; int32_t viewport_h; float display_scale; char last_error[256]; - struct lua_State* L; + struct lua_State *L; + + rg_node_data **chunks; + uint32_t chunk_count; + uint32_t chunk_cap; + uint32_t slot_count; + uint32_t free_head; + uint32_t node_count; + rg_node root; }; -void rg__log(rg_context* ctx, int32_t level, const char* fmt, ...) RG_PRINTF(3, 4); -int rg__fail(rg_context* ctx, int code, const char* fmt, ...) RG_PRINTF(3, 4); +void rg__log(rg_context *ctx, int32_t level, const char *fmt, ...) + RG_PRINTF(3, 4); +int rg__fail(rg_context *ctx, int code, const char *fmt, ...) RG_PRINTF(3, 4); + +void *rg__alloc(rg_context *ctx, size_t size); +void *rg__zalloc(rg_context *ctx, size_t size); +void *rg__realloc(rg_context *ctx, void *ptr, size_t old_size, size_t new_size); +void rg__free(rg_context *ctx, void *ptr, size_t size); + +int rg__nodes_init(rg_context *ctx); +void rg__nodes_free(rg_context *ctx); +rg_node rg__node_create(rg_context *ctx, int32_t type); +rg_node_data *rg__node_resolve(const rg_context *ctx, rg_node node); + +const rg_prop_info *rg__prop_info(int32_t prop); +const rg_type_info *rg__type_info(int32_t type); +const rg_value *rg__default_value(int32_t type, int32_t prop); +void rg__overrides_free(rg_context *ctx, rg_node_data *n); size_t rg__clay_min_memory(void); -void rg__lua_close(rg_context* ctx); +void rg__lua_close(rg_context *ctx); #endif diff --git a/src/nodes.c b/src/nodes.c new file mode 100644 index 0000000..6b43795 --- /dev/null +++ b/src/nodes.c @@ -0,0 +1,276 @@ +#include "internal.h" + +#define CHUNK_SHIFT 8 +#define CHUNK_SIZE (1u << CHUNK_SHIFT) +#define CHUNK_MASK (CHUNK_SIZE - 1u) + +static rg_node_data *slot_at(const rg_context *ctx, uint32_t idx) { + return &ctx->chunks[idx >> CHUNK_SHIFT][idx & CHUNK_MASK]; +} + +static rg_node make_handle(const rg_node_data *n) { + return ((rg_node)n->generation << 32) | (rg_node)n->index; +} + +static rg_node handle_of(const rg_context *ctx, uint32_t idx) { + return idx == RG_NIL ? RG_NODE_NONE : make_handle(slot_at(ctx, idx)); +} + +rg_node_data *rg__node_resolve(const rg_context *ctx, rg_node h) { + uint32_t idx = (uint32_t)(h & 0xFFFFFFFFu); + uint32_t gen = (uint32_t)(h >> 32); + rg_node_data *n; + + if (h == RG_NODE_NONE || idx >= ctx->slot_count) + return NULL; + n = slot_at(ctx, idx); + if (!n->alive || n->generation != gen) + return NULL; + return n; +} + +static int grow_chunks(rg_context *ctx) { + rg_node_data *chunk; + + if (ctx->chunk_count == ctx->chunk_cap) { + uint32_t cap = ctx->chunk_cap ? ctx->chunk_cap * 2 : 4; + rg_node_data **arr = rg__realloc( + ctx, ctx->chunks, ctx->chunk_cap * sizeof *arr, cap * sizeof *arr); + if (!arr) + return RG_ERR_NO_MEMORY; + ctx->chunks = arr; + ctx->chunk_cap = cap; + } + chunk = rg__zalloc(ctx, CHUNK_SIZE * sizeof *chunk); + if (!chunk) + return RG_ERR_NO_MEMORY; + ctx->chunks[ctx->chunk_count++] = chunk; + return RG_OK; +} + +static rg_node_data *slot_alloc(rg_context *ctx) { + rg_node_data *n; + uint32_t idx; + + if (ctx->free_head != RG_NIL) { + idx = ctx->free_head; + n = slot_at(ctx, idx); + ctx->free_head = n->next_free; + } else { + idx = ctx->slot_count; + if ((idx >> CHUNK_SHIFT) == ctx->chunk_count && grow_chunks(ctx) != RG_OK) + return NULL; + n = slot_at(ctx, idx); + ctx->slot_count++; + } + + n->index = idx; + if (n->generation == 0) + n->generation = 1; + n->alive = 1; + n->parent = n->first_child = n->last_child = RG_NIL; + n->prev = n->next = n->next_free = RG_NIL; + n->ov = NULL; + n->ov_count = n->ov_cap = 0; + ctx->node_count++; + return n; +} + +static void release_slot(rg_context *ctx, rg_node_data *n) { + rg__log(ctx, RG_LOG_TRACE, "node destroyed: %s (slot %u)", + rg_type_name(n->type), (unsigned)n->index); + rg__overrides_free(ctx, n); + n->alive = 0; + if (++n->generation == 0) + n->generation = 1; + n->next_free = ctx->free_head; + ctx->free_head = n->index; + ctx->node_count--; +} + +static void unlink_node(rg_context *ctx, rg_node_data *n) { + rg_node_data *p; + + if (n->parent == RG_NIL) + return; + p = slot_at(ctx, n->parent); + if (n->prev != RG_NIL) + slot_at(ctx, n->prev)->next = n->next; + else + p->first_child = n->next; + if (n->next != RG_NIL) + slot_at(ctx, n->next)->prev = n->prev; + else + p->last_child = n->prev; + n->parent = n->prev = n->next = RG_NIL; +} + +static void destroy_subtree(rg_context *ctx, rg_node_data *top) { + uint32_t top_idx = top->index; + uint32_t cur = top_idx; + + for (;;) { + rg_node_data *n = slot_at(ctx, cur); + uint32_t parent; + + if (n->first_child != RG_NIL) { + cur = n->first_child; + continue; + } + parent = n->parent; + unlink_node(ctx, n); + release_slot(ctx, n); + if (cur == top_idx) + break; + cur = parent; + } +} + +static int is_self_or_ancestor(const rg_context *ctx, const rg_node_data *anc, + const rg_node_data *n) { + while (n) { + if (n == anc) + return 1; + n = n->parent == RG_NIL ? NULL : slot_at(ctx, n->parent); + } + return 0; +} + +rg_node rg__node_create(rg_context *ctx, int32_t type) { + rg_node_data *n = slot_alloc(ctx); + + if (!n) { + rg__fail(ctx, RG_ERR_NO_MEMORY, "node allocation failed"); + return RG_NODE_NONE; + } + n->type = type; + rg__log(ctx, RG_LOG_TRACE, "node created: %s (slot %u)", rg_type_name(type), + (unsigned)n->index); + return make_handle(n); +} + +int rg__nodes_init(rg_context *ctx) { + ctx->free_head = RG_NIL; + ctx->root = rg__node_create(ctx, RG_NODE_ROOT); + return ctx->root == RG_NODE_NONE ? RG_ERR_NO_MEMORY : RG_OK; +} + +void rg__nodes_free(rg_context *ctx) { + uint32_t i; + + for (i = 0; i < ctx->slot_count; i++) { + rg_node_data *n = slot_at(ctx, i); + if (n->alive) + rg__overrides_free(ctx, n); + } + for (i = 0; i < ctx->chunk_count; i++) + rg__free(ctx, ctx->chunks[i], CHUNK_SIZE * sizeof(rg_node_data)); + rg__free(ctx, ctx->chunks, ctx->chunk_cap * sizeof *ctx->chunks); + ctx->chunks = NULL; + ctx->chunk_count = ctx->chunk_cap = ctx->slot_count = ctx->node_count = 0; + ctx->root = RG_NODE_NONE; +} + +rg_node rg_root(const rg_context *ctx) { return ctx->root; } + +rg_node rg_node_create(rg_context *ctx, int32_t type) { + if (type <= RG_NODE_ROOT || type >= RG_NODE_TYPE_COUNT) { + rg__fail(ctx, RG_ERR_TYPE, "cannot create node of type %d", (int)type); + return RG_NODE_NONE; + } + return rg__node_create(ctx, type); +} + +void rg_node_destroy(rg_context *ctx, rg_node node) { + rg_node_data *n = rg__node_resolve(ctx, node); + + if (!n) { + rg__log(ctx, RG_LOG_WARN, "destroy of a stale or invalid handle ignored"); + return; + } + if (node == ctx->root) { + rg__log(ctx, RG_LOG_WARN, "the root node cannot be destroyed"); + return; + } + unlink_node(ctx, n); + destroy_subtree(ctx, n); +} + +int rg_node_valid(const rg_context *ctx, rg_node node) { + return rg__node_resolve(ctx, node) != NULL; +} + +int32_t rg_node_type(const rg_context *ctx, rg_node node) { + rg_node_data *n = rg__node_resolve(ctx, node); + return n ? n->type : -1; +} + +int rg_node_insert_before(rg_context *ctx, rg_node parent, rg_node child, + rg_node ref) { + rg_node_data *p = rg__node_resolve(ctx, parent); + rg_node_data *c = rg__node_resolve(ctx, child); + rg_node_data *r = NULL; + + if (!p || !c) + return rg__fail(ctx, RG_ERR_HANDLE, "stale or invalid handle"); + if (child == ctx->root) + return rg__fail(ctx, RG_ERR_STATE, "the root cannot be a child"); + if (ref != RG_NODE_NONE) { + r = rg__node_resolve(ctx, ref); + if (!r) + return rg__fail(ctx, RG_ERR_HANDLE, "stale or invalid reference handle"); + if (r == c || r->parent != p->index) + return rg__fail(ctx, RG_ERR_STATE, + "reference is not a child of the parent"); + } + if (is_self_or_ancestor(ctx, c, p)) + return rg__fail(ctx, RG_ERR_STATE, "insert would create a cycle"); + + unlink_node(ctx, c); + c->parent = p->index; + if (r) { + c->next = r->index; + c->prev = r->prev; + if (r->prev != RG_NIL) + slot_at(ctx, r->prev)->next = c->index; + else + p->first_child = c->index; + r->prev = c->index; + } else { + c->prev = p->last_child; + if (p->last_child != RG_NIL) + slot_at(ctx, p->last_child)->next = c->index; + else + p->first_child = c->index; + p->last_child = c->index; + } + return RG_OK; +} + +int rg_node_append(rg_context *ctx, rg_node parent, rg_node child) { + return rg_node_insert_before(ctx, parent, child, RG_NODE_NONE); +} + +int rg_node_detach(rg_context *ctx, rg_node node) { + rg_node_data *n = rg__node_resolve(ctx, node); + + if (!n) + return rg__fail(ctx, RG_ERR_HANDLE, "stale or invalid handle"); + unlink_node(ctx, n); + return RG_OK; +} + +rg_node rg_node_parent(const rg_context *ctx, rg_node node) { + rg_node_data *n = rg__node_resolve(ctx, node); + return n ? handle_of(ctx, n->parent) : RG_NODE_NONE; +} + +rg_node rg_node_first_child(const rg_context *ctx, rg_node node) { + rg_node_data *n = rg__node_resolve(ctx, node); + return n ? handle_of(ctx, n->first_child) : RG_NODE_NONE; +} + +rg_node rg_node_next_sibling(const rg_context *ctx, rg_node node) { + rg_node_data *n = rg__node_resolve(ctx, node); + return n ? handle_of(ctx, n->next) : RG_NODE_NONE; +} diff --git a/src/props.c b/src/props.c new file mode 100644 index 0000000..3c4eede --- /dev/null +++ b/src/props.c @@ -0,0 +1,251 @@ +#include "internal.h" + +#include + +static const rg_prop_info prop_table[RG_PROP_COUNT] = { + [RG_PROP_DIRECTION] = {"direction", RG_VAL_ENUM, RG_PF_LAYOUT, + RG_V_ENUM(RG_DIR_ROW)}, + [RG_PROP_WIDTH] = {"width", RG_VAL_SIZE, RG_PF_LAYOUT | RG_PF_ANIMATE, + RG_V_SIZE(RG_SIZE_FIT, 0)}, + [RG_PROP_HEIGHT] = {"height", RG_VAL_SIZE, RG_PF_LAYOUT | RG_PF_ANIMATE, + RG_V_SIZE(RG_SIZE_FIT, 0)}, + [RG_PROP_PADDING] = {"padding", RG_VAL_VEC4, RG_PF_LAYOUT | RG_PF_ANIMATE, + RG_V_VEC4(0, 0, 0, 0)}, + [RG_PROP_GAP] = {"gap", RG_VAL_FLOAT, RG_PF_LAYOUT | RG_PF_ANIMATE, + RG_V_FLOAT(0)}, + [RG_PROP_ALIGN_X] = {"align_x", RG_VAL_ENUM, RG_PF_LAYOUT, + RG_V_ENUM(RG_ALIGN_START)}, + [RG_PROP_ALIGN_Y] = {"align_y", RG_VAL_ENUM, RG_PF_LAYOUT, + RG_V_ENUM(RG_ALIGN_START)}, + [RG_PROP_BACKGROUND] = {"background", RG_VAL_COLOR, RG_PF_ANIMATE, + RG_V_COLOR(0, 0, 0, 0)}, + [RG_PROP_BORDER_COLOR] = {"border_color", RG_VAL_COLOR, RG_PF_ANIMATE, + RG_V_COLOR(0, 0, 0, 0)}, + [RG_PROP_BORDER_WIDTH] = {"border_width", RG_VAL_VEC4, + RG_PF_LAYOUT | RG_PF_ANIMATE, + RG_V_VEC4(0, 0, 0, 0)}, + [RG_PROP_CORNER_RADIUS] = {"corner_radius", RG_VAL_VEC4, RG_PF_ANIMATE, + RG_V_VEC4(0, 0, 0, 0)}, + [RG_PROP_OPACITY] = {"opacity", RG_VAL_FLOAT, RG_PF_ANIMATE, RG_V_FLOAT(1)}, + [RG_PROP_VISIBLE] = {"visible", RG_VAL_BOOL, RG_PF_LAYOUT, RG_V_BOOL(1)}, + [RG_PROP_TEXT] = {"text", RG_VAL_STRING, RG_PF_LAYOUT, RG_V_STRING("")}, + [RG_PROP_FONT] = {"font", RG_VAL_STRING, RG_PF_LAYOUT | RG_PF_INHERIT, + RG_V_STRING("")}, + [RG_PROP_FONT_SIZE] = {"font_size", RG_VAL_FLOAT, + RG_PF_LAYOUT | RG_PF_INHERIT, RG_V_FLOAT(16)}, + [RG_PROP_COLOR] = {"color", RG_VAL_COLOR, RG_PF_INHERIT | RG_PF_ANIMATE, + RG_V_COLOR(1, 1, 1, 1)}, +}; + +static const char *val_type_name(int32_t t) { + static const char *names[] = {"none", "bool", "int", "float", "enum", + "color", "size", "vec4", "string"}; + return t >= 0 && t <= RG_VAL_STRING ? names[t] : "invalid"; +} + +static void value_init(rg_value *v, int32_t type) { + memset(v, 0, sizeof *v); + v->type = type; +} + +const rg_prop_info *rg__prop_info(int32_t prop) { + if (prop < 0 || prop >= RG_PROP_COUNT) + return NULL; + return &prop_table[prop]; +} + +int32_t rg_prop_find(const char *name) { + int32_t i; + + if (!name) + return -1; + for (i = 0; i < RG_PROP_COUNT; i++) + if (strcmp(prop_table[i].name, name) == 0) + return i; + return -1; +} + +const char *rg_prop_name(int32_t prop) { + const rg_prop_info *info = rg__prop_info(prop); + return info ? info->name : NULL; +} + +int32_t rg_prop_type(int32_t prop) { + const rg_prop_info *info = rg__prop_info(prop); + return info ? info->type : RG_VAL_NONE; +} + +const rg_value *rg__default_value(int32_t type, int32_t prop) { + const rg_type_info *t = rg__type_info(type); + int32_t i; + + if (t) + for (i = 0; i < t->default_count; i++) + if (t->defaults[i].prop == prop) + return &t->defaults[i].value; + return &prop_table[prop].def; +} + +static char *dup_string(rg_context *ctx, const char *s) { + size_t len = strlen(s) + 1; + char *p = rg__alloc(ctx, len); + + if (p) + memcpy(p, s, len); + return p; +} + +static void drop_value(rg_context *ctx, rg_value *v) { + if (v->type == RG_VAL_STRING && v->s) { + rg__free(ctx, (void *)v->s, strlen(v->s) + 1); + v->s = NULL; + } +} + +void rg__overrides_free(rg_context *ctx, rg_node_data *n) { + uint32_t i; + + for (i = 0; i < n->ov_count; i++) + drop_value(ctx, &n->ov[i].value); + rg__free(ctx, n->ov, n->ov_cap * sizeof *n->ov); + n->ov = NULL; + n->ov_count = n->ov_cap = 0; +} + +static rg_override *find_override(const rg_node_data *n, int32_t prop) { + uint32_t i; + + for (i = 0; i < n->ov_count; i++) + if (n->ov[i].prop == prop) + return &n->ov[i]; + return NULL; +} + +int rg_set(rg_context *ctx, rg_node node, int32_t prop, const rg_value *value) { + rg_node_data *n = rg__node_resolve(ctx, node); + const rg_prop_info *info = rg__prop_info(prop); + rg_override *o; + rg_value v; + + if (!n) + return rg__fail(ctx, RG_ERR_HANDLE, "set: stale or invalid handle"); + if (!info) + return rg__fail(ctx, RG_ERR_PROP, "set: unknown property %d", (int)prop); + if (!value || value->type != info->type) + return rg__fail(ctx, RG_ERR_TYPE, "set: property '%s' expects %s", + info->name, val_type_name(info->type)); + if (value->type == RG_VAL_STRING && !value->s) + return rg__fail(ctx, RG_ERR_TYPE, "set: property '%s' got a null string", + info->name); + if (value->type == RG_VAL_SIZE && + (value->i < RG_SIZE_FIT || value->i > RG_SIZE_PERCENT)) + return rg__fail(ctx, RG_ERR_TYPE, + "set: property '%s' got an invalid size mode", info->name); + + v = *value; + if (v.type == RG_VAL_STRING) { + v.s = dup_string(ctx, value->s); + if (!v.s) + return rg__fail(ctx, RG_ERR_NO_MEMORY, "set: string allocation failed"); + } + + o = find_override(n, prop); + if (o) { + drop_value(ctx, &o->value); + o->value = v; + } else { + if (n->ov_count == n->ov_cap) { + uint32_t cap = n->ov_cap ? n->ov_cap * 2 : 4; + rg_override *arr = + rg__realloc(ctx, n->ov, n->ov_cap * sizeof *arr, cap * sizeof *arr); + if (!arr) { + drop_value(ctx, &v); + return rg__fail(ctx, RG_ERR_NO_MEMORY, + "set: override allocation failed"); + } + n->ov = arr; + n->ov_cap = cap; + } + n->ov[n->ov_count].prop = prop; + n->ov[n->ov_count].value = v; + n->ov_count++; + } + rg__log(ctx, RG_LOG_TRACE, "set %s on %s", info->name, rg_type_name(n->type)); + return RG_OK; +} + +int rg_get(rg_context *ctx, rg_node node, int32_t prop, rg_value *out) { + rg_node_data *n = rg__node_resolve(ctx, node); + const rg_override *o; + + if (!n) + return rg__fail(ctx, RG_ERR_HANDLE, "get: stale or invalid handle"); + if (!rg__prop_info(prop)) + return rg__fail(ctx, RG_ERR_PROP, "get: unknown property %d", (int)prop); + if (!out) + return rg__fail(ctx, RG_ERR_STATE, "get: null output"); + + o = find_override(n, prop); + *out = o ? o->value : *rg__default_value(n->type, prop); + return RG_OK; +} + +int rg_unset(rg_context *ctx, rg_node node, int32_t prop) { + rg_node_data *n = rg__node_resolve(ctx, node); + rg_override *o; + + if (!n) + return rg__fail(ctx, RG_ERR_HANDLE, "unset: stale or invalid handle"); + if (!rg__prop_info(prop)) + return rg__fail(ctx, RG_ERR_PROP, "unset: unknown property %d", (int)prop); + + o = find_override(n, prop); + if (o) { + drop_value(ctx, &o->value); + *o = n->ov[--n->ov_count]; + } + return RG_OK; +} + +int rg_set_int(rg_context *ctx, rg_node node, int32_t prop, int32_t v) { + const rg_prop_info *info = rg__prop_info(prop); + rg_value val; + + if (!info) + return rg__fail(ctx, RG_ERR_PROP, "set: unknown property %d", (int)prop); + if (info->type != RG_VAL_BOOL && info->type != RG_VAL_INT && + info->type != RG_VAL_ENUM) + return rg__fail(ctx, RG_ERR_TYPE, + "set: property '%s' is not an integer type", info->name); + value_init(&val, info->type); + val.i = v; + return rg_set(ctx, node, prop, &val); +} + +int rg_set_float(rg_context *ctx, rg_node node, int32_t prop, float v) { + rg_value val; + + value_init(&val, RG_VAL_FLOAT); + val.f[0] = v; + return rg_set(ctx, node, prop, &val); +} + +int rg_set_string(rg_context *ctx, rg_node node, int32_t prop, const char *v) { + rg_value val; + + value_init(&val, RG_VAL_STRING); + val.s = v; + return rg_set(ctx, node, prop, &val); +} + +int rg_set_color(rg_context *ctx, rg_node node, int32_t prop, float r, float g, + float b, float a) { + rg_value val; + + value_init(&val, RG_VAL_COLOR); + val.f[0] = r; + val.f[1] = g; + val.f[2] = b; + val.f[3] = a; + return rg_set(ctx, node, prop, &val); +} diff --git a/src/types.c b/src/types.c new file mode 100644 index 0000000..d4e4ce2 --- /dev/null +++ b/src/types.c @@ -0,0 +1,62 @@ +#include "internal.h" + +#include + +#define COUNT(a) ((int32_t)(sizeof(a) / sizeof((a)[0]))) + +static const rg_override root_defaults[] = { + {RG_PROP_WIDTH, RG_V_SIZE(RG_SIZE_GROW, 0)}, + {RG_PROP_HEIGHT, RG_V_SIZE(RG_SIZE_GROW, 0)}, +}; + +static const rg_override hbox_defaults[] = { + {RG_PROP_DIRECTION, RG_V_ENUM(RG_DIR_ROW)}, +}; + +static const rg_override vbox_defaults[] = { + {RG_PROP_DIRECTION, RG_V_ENUM(RG_DIR_COLUMN)}, +}; + +static const rg_override panel_defaults[] = { + {RG_PROP_PADDING, RG_V_VEC4(8, 8, 8, 8)}, + {RG_PROP_BACKGROUND, RG_V_COLOR(0.15f, 0.15f, 0.18f, 1.0f)}, +}; + +static const rg_override button_defaults[] = { + {RG_PROP_PADDING, RG_V_VEC4(8, 16, 8, 16)}, + {RG_PROP_BACKGROUND, RG_V_COLOR(0.25f, 0.25f, 0.30f, 1.0f)}, + {RG_PROP_CORNER_RADIUS, RG_V_VEC4(4, 4, 4, 4)}, +}; + +static const rg_type_info type_table[RG_NODE_TYPE_COUNT] = { + [RG_NODE_ROOT] = {"Root", 0, root_defaults, COUNT(root_defaults)}, + [RG_NODE_BOX] = {"Box", 0, NULL, 0}, + [RG_NODE_HBOX] = {"HBox", 0, hbox_defaults, COUNT(hbox_defaults)}, + [RG_NODE_VBOX] = {"VBox", 0, vbox_defaults, COUNT(vbox_defaults)}, + [RG_NODE_PANEL] = {"Panel", 0, panel_defaults, COUNT(panel_defaults)}, + [RG_NODE_LABEL] = {"Label", RG_TF_TEXT, NULL, 0}, + [RG_NODE_BUTTON] = {"Button", RG_TF_TEXT | RG_TF_FOCUSABLE, button_defaults, + COUNT(button_defaults)}, +}; + +const rg_type_info *rg__type_info(int32_t type) { + if (type < 0 || type >= RG_NODE_TYPE_COUNT) + return NULL; + return &type_table[type]; +} + +int32_t rg_type_find(const char *name) { + int32_t i; + + if (!name) + return -1; + for (i = 0; i < RG_NODE_TYPE_COUNT; i++) + if (strcmp(type_table[i].name, name) == 0) + return i; + return -1; +} + +const char *rg_type_name(int32_t type) { + const rg_type_info *t = rg__type_info(type); + return t ? t->name : "invalid"; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9e9ee46..d316db0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,3 +7,7 @@ if(TARGET SDL3::SDL3) target_link_libraries(smoke_sdl3 PRIVATE SDL3::SDL3) add_test(NAME smoke_sdl3 COMMAND smoke_sdl3) endif() + +add_executable(test_nodes nodes.c) +target_link_libraries(test_nodes PRIVATE regolith) +add_test(NAME nodes COMMAND test_nodes) diff --git a/tests/nodes.c b/tests/nodes.c new file mode 100644 index 0000000..fa3b367 --- /dev/null +++ b/tests/nodes.c @@ -0,0 +1,191 @@ +#include "testing.h" + +#include + +static int child_count(rg_context *ctx, rg_node parent) { + int n = 0; + rg_node c; + + for (c = rg_node_first_child(ctx, parent); c != RG_NODE_NONE; + c = rg_node_next_sibling(ctx, c)) + n++; + return n; +} + +static void test_tree(rg_context *ctx) { + rg_node root = rg_root(ctx); + rg_node a = rg_node_create(ctx, RG_NODE_VBOX); + rg_node b = rg_node_create(ctx, RG_NODE_LABEL); + rg_node c = rg_node_create(ctx, RG_NODE_BUTTON); + rg_node d = rg_node_create(ctx, RG_NODE_LABEL); + + CHECK(rg_node_valid(ctx, root)); + CHECK(rg_node_type(ctx, root) == RG_NODE_ROOT); + CHECK(rg_node_create(ctx, RG_NODE_ROOT) == RG_NODE_NONE); + + CHECK(rg_node_append(ctx, root, a) == RG_OK); + CHECK(rg_node_append(ctx, a, b) == RG_OK); + CHECK(rg_node_append(ctx, a, c) == RG_OK); + CHECK(rg_node_insert_before(ctx, a, d, c) == RG_OK); + + CHECK(child_count(ctx, a) == 3); + CHECK(rg_node_first_child(ctx, a) == b); + CHECK(rg_node_next_sibling(ctx, b) == d); + CHECK(rg_node_next_sibling(ctx, d) == c); + CHECK(rg_node_next_sibling(ctx, c) == RG_NODE_NONE); + CHECK(rg_node_parent(ctx, d) == a); + + CHECK(rg_node_append(ctx, a, b) == RG_OK); + CHECK(rg_node_next_sibling(ctx, c) == b); + CHECK(child_count(ctx, a) == 3); + + CHECK(rg_node_append(ctx, b, a) == RG_ERR_STATE); + CHECK(rg_node_append(ctx, a, a) == RG_ERR_STATE); + CHECK(rg_node_append(ctx, a, root) == RG_ERR_STATE); + CHECK(rg_node_insert_before(ctx, root, b, c) == RG_ERR_STATE); + + CHECK(rg_node_detach(ctx, d) == RG_OK); + CHECK(rg_node_parent(ctx, d) == RG_NODE_NONE); + CHECK(child_count(ctx, a) == 2); + + rg_node_destroy(ctx, a); + rg_node_destroy(ctx, d); +} + +static void test_handles(rg_context *ctx) { + rg_node root = rg_root(ctx); + rg_node a = rg_node_create(ctx, RG_NODE_PANEL); + rg_node b = rg_node_create(ctx, RG_NODE_LABEL); + rg_node c = rg_node_create(ctx, RG_NODE_LABEL); + rg_node fresh; + + CHECK(rg_node_append(ctx, root, a) == RG_OK); + CHECK(rg_node_append(ctx, a, b) == RG_OK); + CHECK(rg_node_append(ctx, b, c) == RG_OK); + + rg_node_destroy(ctx, a); + CHECK(!rg_node_valid(ctx, a)); + CHECK(!rg_node_valid(ctx, b)); + CHECK(!rg_node_valid(ctx, c)); + CHECK(rg_set_float(ctx, b, RG_PROP_GAP, 1.0f) == RG_ERR_HANDLE); + CHECK(child_count(ctx, root) == 0); + + fresh = rg_node_create(ctx, RG_NODE_BOX); + CHECK(fresh != a && fresh != b && fresh != c); + CHECK(!rg_node_valid(ctx, a)); + CHECK(rg_node_valid(ctx, fresh)); + + rg_node_destroy(ctx, root); + CHECK(rg_node_valid(ctx, root)); + rg_node_destroy(ctx, fresh); +} + +static void test_props(rg_context *ctx) { + rg_node hbox = rg_node_create(ctx, RG_NODE_HBOX); + rg_node vbox = rg_node_create(ctx, RG_NODE_VBOX); + rg_node btn = rg_node_create(ctx, RG_NODE_BUTTON); + rg_node lbl = rg_node_create(ctx, RG_NODE_LABEL); + rg_value v; + char buf[16]; + int i; + + for (i = 0; i < RG_PROP_COUNT; i++) { + CHECK(rg_prop_name(i) != NULL); + CHECK(rg_prop_find(rg_prop_name(i)) == i); + CHECK(rg_prop_type(i) != RG_VAL_NONE); + } + CHECK(rg_prop_find("no_such_prop") == -1); + CHECK(rg_type_find("Button") == RG_NODE_BUTTON); + CHECK(rg_type_find("Nope") == -1); + + CHECK(rg_get(ctx, hbox, RG_PROP_DIRECTION, &v) == RG_OK && v.i == RG_DIR_ROW); + CHECK(rg_get(ctx, vbox, RG_PROP_DIRECTION, &v) == RG_OK && + v.i == RG_DIR_COLUMN); + CHECK(rg_get(ctx, btn, RG_PROP_PADDING, &v) == RG_OK); + CHECK(v.f[0] == 8.0f && v.f[1] == 16.0f && v.f[2] == 8.0f && v.f[3] == 16.0f); + CHECK(rg_get(ctx, lbl, RG_PROP_OPACITY, &v) == RG_OK && v.f[0] == 1.0f); + CHECK(rg_get(ctx, lbl, RG_PROP_TEXT, &v) == RG_OK && strcmp(v.s, "") == 0); + + CHECK(rg_set_int(ctx, vbox, RG_PROP_DIRECTION, RG_DIR_ROW) == RG_OK); + CHECK(rg_get(ctx, vbox, RG_PROP_DIRECTION, &v) == RG_OK && v.i == RG_DIR_ROW); + CHECK(rg_unset(ctx, vbox, RG_PROP_DIRECTION) == RG_OK); + CHECK(rg_get(ctx, vbox, RG_PROP_DIRECTION, &v) == RG_OK && + v.i == RG_DIR_COLUMN); + CHECK(rg_unset(ctx, vbox, RG_PROP_DIRECTION) == RG_OK); + + CHECK(rg_set_color(ctx, btn, RG_PROP_BACKGROUND, 1, 0, 0, 1) == RG_OK); + CHECK(rg_get(ctx, btn, RG_PROP_BACKGROUND, &v) == RG_OK && v.f[0] == 1.0f && + v.f[1] == 0.0f); + + CHECK(rg_set_float(ctx, btn, RG_PROP_BACKGROUND, 1.0f) == RG_ERR_TYPE); + CHECK(rg_set_int(ctx, btn, RG_PROP_GAP, 1) == RG_ERR_TYPE); + CHECK(rg_set(ctx, btn, RG_PROP_GAP, NULL) == RG_ERR_TYPE); + CHECK(rg_set_string(ctx, btn, RG_PROP_TEXT, NULL) == RG_ERR_TYPE); + CHECK(rg_set_float(ctx, btn, 9999, 1.0f) == RG_ERR_PROP); + CHECK(rg_get(ctx, btn, -1, &v) == RG_ERR_PROP); + + memset(&v, 0, sizeof v); + v.type = RG_VAL_SIZE; + v.i = 99; + CHECK(rg_set(ctx, btn, RG_PROP_WIDTH, &v) == RG_ERR_TYPE); + v.i = RG_SIZE_FIXED; + v.f[0] = 200.0f; + CHECK(rg_set(ctx, btn, RG_PROP_WIDTH, &v) == RG_OK); + + strcpy(buf, "Start"); + CHECK(rg_set_string(ctx, lbl, RG_PROP_TEXT, buf) == RG_OK); + strcpy(buf, "Other"); + CHECK(rg_get(ctx, lbl, RG_PROP_TEXT, &v) == RG_OK && + strcmp(v.s, "Start") == 0); + + CHECK(rg_set_string(ctx, lbl, RG_PROP_TEXT, v.s) == RG_OK); + CHECK(rg_get(ctx, lbl, RG_PROP_TEXT, &v) == RG_OK && + strcmp(v.s, "Start") == 0); + + CHECK(rg_set_string(ctx, lbl, RG_PROP_TEXT, + "A much longer replacement string") == RG_OK); + CHECK(rg_get(ctx, lbl, RG_PROP_TEXT, &v) == RG_OK && + strcmp(v.s, "A much longer replacement string") == 0); + + for (i = 0; i < RG_PROP_COUNT; i++) { + rg_value tmp; + memset(&tmp, 0, sizeof tmp); + tmp.type = rg_prop_type(i); + tmp.s = tmp.type == RG_VAL_STRING ? "x" : NULL; + if (tmp.type == RG_VAL_SIZE) + tmp.i = RG_SIZE_FIT; + CHECK(rg_set(ctx, btn, i, &tmp) == RG_OK); + } + + rg_node_destroy(ctx, hbox); + rg_node_destroy(ctx, vbox); + rg_node_destroy(ctx, btn); + rg_node_destroy(ctx, lbl); +} + +int main(void) { + testing_alloc ta = {0, 0}; + rg_config cfg; + rg_context *ctx; + + rg_config_init(&cfg); + testing_use_counting_allocator(&cfg, &ta); + cfg.log_level = RG_LOG_WARN; + + ctx = rg_create(&cfg); + CHECK(ctx != NULL); + if (!ctx) + return 1; + + test_tree(ctx); + test_handles(ctx); + test_props(ctx); + + rg_destroy(ctx); + CHECK(ta.live == 0); + if (ta.live != 0) + printf("leak: %zu bytes still live\n", ta.live); + + printf("nodes: %s, peak %zu bytes\n", g_failures ? "FAILED" : "ok", ta.peak); + return g_failures ? 1 : 0; +} diff --git a/tests/testing.h b/tests/testing.h new file mode 100644 index 0000000..a4799b2 --- /dev/null +++ b/tests/testing.h @@ -0,0 +1,58 @@ + +#ifndef RG_TESTING_H +#define RG_TESTING_H + +#include "regolith.h" + +#include +#include + +static int g_failures; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + g_failures++; \ + } \ + } while (0) + +typedef struct testing_alloc { + size_t live; + size_t peak; +} testing_alloc; + +static void ta_track(testing_alloc *t, size_t add, size_t sub) { + t->live = t->live + add - sub; + if (t->live > t->peak) + t->peak = t->live; +} + +static void *ta_alloc(size_t size, void *user) { + void *p = malloc(size); + if (p) + ta_track(user, size, 0); + return p; +} + +static void *ta_realloc(void *ptr, size_t old_size, size_t new_size, + void *user) { + void *p = realloc(ptr, new_size); + if (p) + ta_track(user, new_size, old_size); + return p; +} + +static void ta_free(void *ptr, size_t size, void *user) { + ta_track(user, 0, size); + free(ptr); +} + +static void testing_use_counting_allocator(rg_config *cfg, testing_alloc *t) { + cfg->allocator.alloc = ta_alloc; + cfg->allocator.realloc = ta_realloc; + cfg->allocator.free = ta_free; + cfg->allocator.user = t; +} + +#endif