commit 3ec890af0b7bbd284534604eea2164ebd71f109a
Author: MrJensK <jens.se@icloud.com>
AuthorDate: Mon Aug 24 15:59:59 2026 +0200
Commit: MrJensK <jens.se@icloud.com>
CommitDate: Mon Aug 24 16:00:09 2026 +0200
Initial commit: Hush markdown editor
WYSIWYG markdown editor in C using Clay + raylib, with undo/redo,
text selection, a status bar, and a config file for theming.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
.gitignore | 6 +
CMakeLists.txt | 77 +
README.md | 86 +
assets/fonts/DejaVuSans-Bold.ttf | Bin 0 -> 708920 bytes
assets/fonts/DejaVuSans-BoldOblique.ttf | Bin 0 -> 645600 bytes
assets/fonts/DejaVuSans-Oblique.ttf | Bin 0 -> 637648 bytes
assets/fonts/DejaVuSans.ttf | Bin 0 -> 759720 bytes
assets/fonts/DejaVuSansMono-Bold.ttf | Bin 0 -> 334268 bytes
assets/fonts/DejaVuSansMono-BoldOblique.ttf | Bin 0 -> 254960 bytes
assets/fonts/DejaVuSansMono-Oblique.ttf | Bin 0 -> 253448 bytes
assets/fonts/DejaVuSansMono.ttf | Bin 0 -> 343140 bytes
example.md | 23 +
src/app_mode.h | 21 +
src/clay_impl.c | 3 +
src/config.c | 160 +
src/config.h | 27 +
src/document.c | 55 +
src/document.h | 48 +
src/editor.c | 427 +++
src/editor.h | 63 +
src/fonts.c | 61 +
src/fonts.h | 28 +
src/hittest.c | 176 +
src/hittest.h | 38 +
src/inline_parse.c | 117 +
src/inline_parse.h | 32 +
src/layout_cache.c | 55 +
src/layout_cache.h | 38 +
src/main.c | 183 +
src/markdown_io.c | 167 +
src/markdown_io.h | 26 +
src/render.c | 507 +++
src/render.h | 29 +
src/strbuf.c | 59 +
src/strbuf.h | 20 +
src/test_main.c | 439 +++
src/theme.c | 9 +
src/theme.h | 57 +
src/undo.c | 82 +
src/undo.h | 47 +
src/utf8.h | 24 +
src/wrap.c | 147 +
src/wrap.h | 38 +
vendor/clay.h | 5058 +++++++++++++++++++++++++++
vendor/clay_renderer_raylib.c | 321 ++
45 files changed, 8754 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3132455
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+build/
+build_verify/
+*.o
+compile_commands.json
+test_numbered.md
+test_roundtrip.md
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..3cd53c0
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,77 @@
+cmake_minimum_required(VERSION 3.15)
+project(hush C)
+
+set(CMAKE_C_STANDARD 11)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+if(NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE Release)
+endif()
+
+include(FetchContent)
+
+set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
+set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
+set(SUPPORT_MODULE_RAUDIO OFF CACHE BOOL "" FORCE)
+
+FetchContent_Declare(
+ raylib
+ GIT_REPOSITORY https://github.com/raysan5/raylib.git
+ GIT_TAG 5.5
+ GIT_SHALLOW TRUE
+)
+FetchContent_MakeAvailable(raylib)
+
+add_executable(hush
+ src/clay_impl.c
+ src/strbuf.c
+ src/document.c
+ src/inline_parse.c
+ src/wrap.c
+ src/fonts.c
+ src/layout_cache.c
+ src/hittest.c
+ src/theme.c
+ src/undo.c
+ src/config.c
+ src/editor.c
+ src/render.c
+ src/markdown_io.c
+ src/main.c
+)
+
+target_include_directories(hush PRIVATE vendor)
+target_link_libraries(hush PRIVATE raylib m)
+target_compile_definitions(hush PRIVATE ASSETS_DIR="${CMAKE_SOURCE_DIR}/assets")
+
+if (UNIX AND NOT APPLE)
+ target_link_libraries(hush PRIVATE dl pthread)
+endif()
+
+target_compile_options(hush PRIVATE -Wall -Wextra -Wno-unused-parameter)
+
+# Headless logic test: no window/GPU context is created, so it can run in CI or over SSH.
+add_executable(hush_test
+ src/clay_impl.c
+ src/strbuf.c
+ src/document.c
+ src/inline_parse.c
+ src/wrap.c
+ src/fonts.c
+ src/layout_cache.c
+ src/hittest.c
+ src/theme.c
+ src/undo.c
+ src/config.c
+ src/editor.c
+ src/render.c
+ src/markdown_io.c
+ src/test_main.c
+)
+target_include_directories(hush_test PRIVATE vendor)
+target_link_libraries(hush_test PRIVATE raylib m)
+target_compile_definitions(hush_test PRIVATE ASSETS_DIR="${CMAKE_SOURCE_DIR}/assets")
+if (UNIX AND NOT APPLE)
+ target_link_libraries(hush_test PRIVATE dl pthread)
+endif()
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6d0c817
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+# Hush
+
+A small WYSIWYG markdown editor, built in C with [Clay](https://github.com/nicbarker/clay) and [raylib](https://www.raylib.com/).
+
+Markdown syntax you type (`# `, `**bold**`, `` `code` ``, `- `, `1. `, `> `, `` ``` ``, `[text](url)`) is rendered as styled text as you go, rather than shown as raw markup — the underlying markup only reappears around a span while your cursor is inside it.
+
+## Features
+
+- Headings, bold, italic, inline code, links, block quotes, code blocks, bullet and numbered lists
+- Text selection (Shift+arrows or click-drag) with copy/paste
+- Undo/redo
+- A status bar showing the document name, size, and line count
+- A config file for font, text color, and background color
+- Full Swedish/Latin-1 character support (åäö, etc.)
+
+## Building
+
+Requires CMake 3.15+, a C11 compiler, and a network connection on first build (raylib is fetched automatically via CMake's `FetchContent`).
+
+```sh
+cmake -B build
+cmake --build build
+```
+
+This produces two binaries in `build/`:
+
+- `hush` — the editor itself
+- `hush_test` — a headless test suite for the document/editor logic (no window or GPU context needed, so it can run over SSH or in CI)
+
+## Usage
+
+```sh
+./build/hush [file.md]
+```
+
+Run without an argument to start with an empty, untitled document.
+
+### Keyboard shortcuts
+
+| Shortcut | Action |
+|---|---|
+| Arrow keys | Move the cursor |
+| Shift + arrow keys | Extend the selection |
+| Click + drag | Select text with the mouse |
+| Ctrl+S | Save |
+| Ctrl+C / Ctrl+V | Copy / paste |
+| Ctrl+Z / Ctrl+Shift+Z | Undo / redo |
+| Ctrl+X | Quit (prompts to save if there are unsaved changes) |
+| F1 (or Ctrl+/) | Show this shortcut list |
+| Enter | New line / new block |
+| Backspace / Delete | Delete a character (merges/demotes blocks at boundaries) |
+| Home / End | Start / end of the current line |
+
+The window's close button goes through the same unsaved-changes prompt as Ctrl+X.
+
+## Configuration
+
+On first run, Hush creates a config file at `$XDG_CONFIG_HOME/hush/config` (or `~/.config/hush/config` if that variable isn't set), with every available key present but commented out. Uncomment and edit a line to change it; unknown or malformed lines are ignored.
+
+```
+text_color = #1F2328
+bg_color = #FDFDFB
+
+font_sans_regular = /path/to/font.ttf
+font_sans_bold = /path/to/font-bold.ttf
+font_sans_italic = /path/to/font-italic.ttf
+font_sans_bold_italic = /path/to/font-bold-italic.ttf
+font_mono_regular = /path/to/mono.ttf
+font_mono_bold = /path/to/mono-bold.ttf
+font_mono_italic = /path/to/mono-italic.ttf
+font_mono_bold_italic = /path/to/mono-bold-italic.ttf
+```
+
+Font paths are optional per style; any left unset fall back to the bundled DejaVu fonts. `text_color` and `bg_color` accept `#RRGGBB` (the `#` is optional).
+
+## Testing
+
+```sh
+./build/hush_test
+```
+
+Runs the full logic test suite (document editing, markdown round-tripping, undo/redo, selection, config parsing, ...) and prints a pass/fail summary.
+
+## Acknowledgments
+
+Built with [Clay](https://github.com/nicbarker/clay) and [raylib](https://www.raylib.com/), and bundles the [DejaVu fonts](https://dejavu-fonts.github.io/).
diff --git a/assets/fonts/DejaVuSans-Bold.ttf b/assets/fonts/DejaVuSans-Bold.ttf
new file mode 100644
index 0000000..0a7fb3f
Binary files /dev/null and b/assets/fonts/DejaVuSans-Bold.ttf differ
diff --git a/assets/fonts/DejaVuSans-BoldOblique.ttf b/assets/fonts/DejaVuSans-BoldOblique.ttf
new file mode 100644
index 0000000..e68b9ba
Binary files /dev/null and b/assets/fonts/DejaVuSans-BoldOblique.ttf differ
diff --git a/assets/fonts/DejaVuSans-Oblique.ttf b/assets/fonts/DejaVuSans-Oblique.ttf
new file mode 100644
index 0000000..59c3198
Binary files /dev/null and b/assets/fonts/DejaVuSans-Oblique.ttf differ
diff --git a/assets/fonts/DejaVuSans.ttf b/assets/fonts/DejaVuSans.ttf
new file mode 100644
index 0000000..1728e9e
Binary files /dev/null and b/assets/fonts/DejaVuSans.ttf differ
diff --git a/assets/fonts/DejaVuSansMono-Bold.ttf b/assets/fonts/DejaVuSansMono-Bold.ttf
new file mode 100644
index 0000000..d4fc522
Binary files /dev/null and b/assets/fonts/DejaVuSansMono-Bold.ttf differ
diff --git a/assets/fonts/DejaVuSansMono-BoldOblique.ttf b/assets/fonts/DejaVuSansMono-BoldOblique.ttf
new file mode 100644
index 0000000..39cdaa5
Binary files /dev/null and b/assets/fonts/DejaVuSansMono-BoldOblique.ttf differ
diff --git a/assets/fonts/DejaVuSansMono-Oblique.ttf b/assets/fonts/DejaVuSansMono-Oblique.ttf
new file mode 100644
index 0000000..47b50f4
Binary files /dev/null and b/assets/fonts/DejaVuSansMono-Oblique.ttf differ
diff --git a/assets/fonts/DejaVuSansMono.ttf b/assets/fonts/DejaVuSansMono.ttf
new file mode 100644
index 0000000..5a6544a
Binary files /dev/null and b/assets/fonts/DejaVuSansMono.ttf differ
diff --git a/example.md b/example.md
new file mode 100644
index 0000000..7977d6b
--- /dev/null
+++ b/example.md
@@ -0,0 +1,23 @@
+# Hush
+
+En liten **äkta** WYSIWYG-redigerare för *markdown*, byggd i C och `clay.h`.
+
+## Funktioner
+
+- Rubriker, **fetstil** och *kursiv* text
+- Punktlistor och numrerade listor
+- Inline-kod som `printf("hej")` och länkar som [Clay](https://github.com/nicbarker/clay)
+
+1. Först
+2. Sedan
+3. Till sist
+
+> Ett citat med lite mer text som förhoppningsvis radbryts fint när det blir tillräckligt långt för att testa ordbrytningen ordentligt.
+
+```
+int main(void) {
+ return 0;
+}
+```
+
+Vanlig text med åäö för att testa svenska tecken.
diff --git a/src/app_mode.h b/src/app_mode.h
new file mode 100644
index 0000000..ac42ccd
--- /dev/null
+++ b/src/app_mode.h
@@ -0,0 +1,21 @@
+#ifndef HUSH_APP_MODE_H
+#define HUSH_APP_MODE_H
+
+/* Presentation/flow state -- deliberately kept out of EditorState, which is document-editing
+ state. Threaded through main.c's loop and into render_frame alongside the other per-frame
+ values (e.g. caretBlinkT) it already passes explicitly. */
+typedef enum {
+ MODE_EDITING,
+ MODE_QUIT_CONFIRM,
+ MODE_SHORTCUTS_HELP,
+} AppMode;
+
+typedef enum {
+ MODAL_ACTION_NONE,
+ MODAL_ACTION_SAVE_QUIT,
+ MODAL_ACTION_DISCARD_QUIT,
+ MODAL_ACTION_CANCEL,
+ MODAL_ACTION_CLOSE_HELP,
+} ModalAction;
+
+#endif
diff --git a/src/clay_impl.c b/src/clay_impl.c
new file mode 100644
index 0000000..b22f9f3
--- /dev/null
+++ b/src/clay_impl.c
@@ -0,0 +1,3 @@
+#define CLAY_IMPLEMENTATION
+#include "clay.h"
+#include "clay_renderer_raylib.c"
diff --git a/src/config.c b/src/config.c
new file mode 100644
index 0000000..0ec6bc2
--- /dev/null
+++ b/src/config.c
@@ -0,0 +1,160 @@
+#include "config.h"
+#include "theme.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <sys/stat.h>
+
+static const char *FONT_KEY_NAMES[FONT_COUNT] = {
+ "font_sans_regular", "font_sans_bold", "font_sans_italic", "font_sans_bold_italic",
+ "font_mono_regular", "font_mono_bold", "font_mono_italic", "font_mono_bold_italic",
+};
+
+static void config_set_defaults(Config *cfg) {
+ cfg->textColor = DEFAULT_COL_TEXT;
+ cfg->bgColor = DEFAULT_COL_BG;
+ for (int i = 0; i < FONT_COUNT; i++) cfg->fontPaths[i] = NULL;
+}
+
+void config_free(Config *cfg) {
+ for (int i = 0; i < FONT_COUNT; i++) {
+ free(cfg->fontPaths[i]);
+ cfg->fontPaths[i] = NULL;
+ }
+}
+
+static const char *trim(const char *s, int len, int *outLen) {
+ while (len > 0 && isspace((unsigned char)s[0])) { s++; len--; }
+ while (len > 0 && isspace((unsigned char)s[len - 1])) len--;
+ *outLen = len;
+ return s;
+}
+
+static bool parse_hex_byte(const char *s, unsigned char *out) {
+ unsigned char v = 0;
+ for (int i = 0; i < 2; i++) {
+ char c = s[i];
+ v <<= 4;
+ if (c >= '0' && c <= '9') v |= (unsigned char)(c - '0');
+ else if (c >= 'a' && c <= 'f') v |= (unsigned char)(c - 'a' + 10);
+ else if (c >= 'A' && c <= 'F') v |= (unsigned char)(c - 'A' + 10);
+ else return false;
+ }
+ *out = v;
+ return true;
+}
+
+static bool parse_hex_color(const char *v, int len, Clay_Color *out) {
+ if (len > 0 && v[0] == '#') { v++; len--; }
+ if (len != 6) return false;
+ unsigned char r, g, b;
+ if (!parse_hex_byte(v, &r) || !parse_hex_byte(v + 2, &g) || !parse_hex_byte(v + 4, &b)) return false;
+ out->r = r; out->g = g; out->b = b; out->a = 255;
+ return true;
+}
+
+bool config_parse_line(const char *line, int len, Config *cfg) {
+ int trimmedLen;
+ const char *t = trim(line, len, &trimmedLen);
+ if (trimmedLen == 0 || t[0] == '#') return false;
+
+ const char *eq = memchr(t, '=', (size_t)trimmedLen);
+ if (!eq) return false;
+
+ int keyLen;
+ const char *key = trim(t, (int)(eq - t), &keyLen);
+ int valueLen;
+ const char *value = trim(eq + 1, (int)(t + trimmedLen - (eq + 1)), &valueLen);
+ if (keyLen == 0 || valueLen == 0) return false;
+
+ if (keyLen == 10 && memcmp(key, "text_color", 10) == 0) return parse_hex_color(value, valueLen, &cfg->textColor);
+ if (keyLen == 8 && memcmp(key, "bg_color", 8) == 0) return parse_hex_color(value, valueLen, &cfg->bgColor);
+
+ for (int i = 0; i < FONT_COUNT; i++) {
+ size_t nameLen = strlen(FONT_KEY_NAMES[i]);
+ if ((size_t)keyLen == nameLen && memcmp(key, FONT_KEY_NAMES[i], nameLen) == 0) {
+ free(cfg->fontPaths[i]);
+ cfg->fontPaths[i] = malloc((size_t)valueLen + 1);
+ memcpy(cfg->fontPaths[i], value, (size_t)valueLen);
+ cfg->fontPaths[i][valueLen] = '\0';
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool build_config_path(char *buf, size_t bufSize) {
+ const char *xdg = getenv("XDG_CONFIG_HOME");
+ if (xdg && xdg[0]) { snprintf(buf, bufSize, "%s/hush/config", xdg); return true; }
+ const char *home = getenv("HOME");
+ if (!home || !home[0]) return false;
+ snprintf(buf, bufSize, "%s/.config/hush/config", home);
+ return true;
+}
+
+/* Creates every directory component of `path` (a file path; only the directory portion is
+ created). Tolerates components that already exist. */
+static void mkdir_parents(const char *path) {
+ char buf[1024];
+ size_t n = strlen(path);
+ if (n >= sizeof buf) return;
+ memcpy(buf, path, n + 1);
+
+ for (size_t i = 1; i < n; i++) {
+ if (buf[i] != '/') continue;
+ buf[i] = '\0';
+ mkdir(buf, 0755);
+ buf[i] = '/';
+ }
+}
+
+static void write_default_config(const char *path) {
+ mkdir_parents(path);
+ FILE *f = fopen(path, "wb");
+ if (!f) return;
+ fprintf(f,
+ "# hush config file\n"
+ "# Uncomment and edit any line below to override the default. Unknown or malformed\n"
+ "# lines are ignored, so it's safe to experiment.\n"
+ "\n"
+ "# text_color = #%02X%02X%02X\n"
+ "# bg_color = #%02X%02X%02X\n"
+ "\n"
+ "# Font overrides (paths to .ttf files). Relative paths are resolved relative to the\n"
+ "# directory hush is launched from. Leave commented out to use the bundled DejaVu fonts.\n"
+ "# font_sans_regular = /path/to/font.ttf\n"
+ "# font_sans_bold = /path/to/font-bold.ttf\n"
+ "# font_sans_italic = /path/to/font-italic.ttf\n"
+ "# font_sans_bold_italic = /path/to/font-bold-italic.ttf\n"
+ "# font_mono_regular = /path/to/mono.ttf\n"
+ "# font_mono_bold = /path/to/mono-bold.ttf\n"
+ "# font_mono_italic = /path/to/mono-italic.ttf\n"
+ "# font_mono_bold_italic = /path/to/mono-bold-italic.ttf\n",
+ (unsigned)DEFAULT_COL_TEXT.r, (unsigned)DEFAULT_COL_TEXT.g, (unsigned)DEFAULT_COL_TEXT.b,
+ (unsigned)DEFAULT_COL_BG.r, (unsigned)DEFAULT_COL_BG.g, (unsigned)DEFAULT_COL_BG.b);
+ fclose(f);
+}
+
+bool config_load(Config *out) {
+ config_set_defaults(out);
+
+ char path[1024];
+ if (!build_config_path(path, sizeof path)) return true;
+
+ FILE *f = fopen(path, "rb");
+ if (!f) {
+ write_default_config(path);
+ return true;
+ }
+
+ char line[1024];
+ while (fgets(line, sizeof line, f)) {
+ int len = (int)strlen(line);
+ while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) len--;
+ config_parse_line(line, len, out);
+ }
+ fclose(f);
+ return true;
+}
diff --git a/src/config.h b/src/config.h
new file mode 100644
index 0000000..5ca17ac
--- /dev/null
+++ b/src/config.h
@@ -0,0 +1,27 @@
+#ifndef HUSH_CONFIG_H
+#define HUSH_CONFIG_H
+
+#include "clay.h"
+#include "fonts.h"
+#include <stdbool.h>
+
+typedef struct {
+ Clay_Color textColor;
+ Clay_Color bgColor;
+ char *fontPaths[FONT_COUNT]; /* malloc'd override path per slot, or NULL = bundled default */
+} Config;
+
+/* Fills `out` with the built-in defaults, then overlays whatever is set in the user's config
+ file (XDG_CONFIG_HOME/hush/config, or ~/.config/hush/config). If no config file exists yet,
+ creates one there with every key present but commented out, showing the defaults, so it's
+ self-documenting on first run. Always returns true -- filesystem failures (can't read, can't
+ create the default file, no $HOME) are non-fatal and just leave `out` at its defaults. */
+bool config_load(Config *out);
+void config_free(Config *cfg);
+
+/* Exposed for headless unit testing of the line parser without touching a real file. Returns
+ true if the line set a recognized key on `cfg`; false for comments/blank/unrecognized/
+ malformed lines -- never fatal, so a typo in a hand-edited file can't stop hush from starting. */
+bool config_parse_line(const char *line, int len, Config *cfg);
+
+#endif
diff --git a/src/document.c b/src/document.c
new file mode 100644
index 0000000..ef7f308
--- /dev/null
+++ b/src/document.c
@@ -0,0 +1,55 @@
+#include "document.h"
+#include <stdlib.h>
+#include <string.h>
+
+static void ensure_cap(Document *doc, int needed) {
+ if (needed <= doc->cap) return;
+ int newCap = doc->cap > 0 ? doc->cap : 8;
+ while (newCap < needed) newCap *= 2;
+ doc->blocks = realloc(doc->blocks, (size_t)newCap * sizeof(Block));
+ doc->cap = newCap;
+}
+
+void document_init(Document *doc) {
+ doc->blocks = NULL;
+ doc->count = 0;
+ doc->cap = 0;
+ document_insert_block(doc, 0, BLOCK_PARAGRAPH, "", 0);
+}
+
+void document_free(Document *doc) {
+ for (int i = 0; i < doc->count; i++) {
+ sb_free(&doc->blocks[i].text);
+ }
+ free(doc->blocks);
+ doc->blocks = NULL;
+ doc->count = 0;
+ doc->cap = 0;
+}
+
+void document_insert_block(Document *doc, int index, BlockType type, const char *text, int len) {
+ ensure_cap(doc, doc->count + 1);
+ memmove(&doc->blocks[index + 1], &doc->blocks[index], (size_t)(doc->count - index) * sizeof(Block));
+ doc->count++;
+ Block *b = &doc->blocks[index];
+ b->type = type;
+ sb_init(&b->text);
+ sb_append(&b->text, text, len);
+}
+
+void document_clone(Document *dst, const Document *src) {
+ dst->count = src->count;
+ dst->cap = src->count;
+ dst->blocks = dst->cap > 0 ? malloc((size_t)dst->cap * sizeof(Block)) : NULL;
+ for (int i = 0; i < src->count; i++) {
+ dst->blocks[i].type = src->blocks[i].type;
+ sb_init(&dst->blocks[i].text);
+ sb_append(&dst->blocks[i].text, src->blocks[i].text.data, src->blocks[i].text.len);
+ }
+}
+
+void document_remove_block(Document *doc, int index) {
+ sb_free(&doc->blocks[index].text);
+ memmove(&doc->blocks[index], &doc->blocks[index + 1], (size_t)(doc->count - index - 1) * sizeof(Block));
+ doc->count--;
+}
diff --git a/src/document.h b/src/document.h
new file mode 100644
index 0000000..34b76ea
--- /dev/null
+++ b/src/document.h
@@ -0,0 +1,48 @@
+#ifndef HUSH_DOCUMENT_H
+#define HUSH_DOCUMENT_H
+
+#include "strbuf.h"
+
+typedef enum {
+ BLOCK_PARAGRAPH,
+ BLOCK_H1,
+ BLOCK_H2,
+ BLOCK_H3,
+ BLOCK_BULLET,
+ BLOCK_NUMBERED,
+ BLOCK_QUOTE,
+ BLOCK_CODE,
+} BlockType;
+
+typedef struct {
+ BlockType type;
+ StrBuf text; /* raw text, without block-level prefix. Code blocks may embed '\n'. */
+} Block;
+
+typedef struct {
+ Block *blocks;
+ int count;
+ int cap;
+} Document;
+
+/* True for block types that "continue" themselves on Enter (list-like blocks). */
+static inline int block_type_is_continuable(BlockType t) {
+ return t == BLOCK_BULLET || t == BLOCK_NUMBERED || t == BLOCK_QUOTE;
+}
+
+void document_init(Document *doc);
+void document_free(Document *doc);
+
+/* Deep-copies `src` into `dst`. `dst` must not already hold live blocks (it is treated as
+ uninitialized, not freed first) — used to snapshot state for undo/redo. */
+void document_clone(Document *dst, const Document *src);
+
+/* Inserts a new block at `index`, shifting later blocks right. Text is copied. */
+void document_insert_block(Document *doc, int index, BlockType type, const char *text, int len);
+void document_remove_block(Document *doc, int index);
+
+static inline Block *document_block(Document *doc, int index) {
+ return &doc->blocks[index];
+}
+
+#endif
diff --git a/src/editor.c b/src/editor.c
new file mode 100644
index 0000000..0126c0b
--- /dev/null
+++ b/src/editor.c
@@ -0,0 +1,427 @@
+#include "editor.h"
+#include "markdown_io.h"
+#include "hittest.h"
+#include "utf8.h"
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+static void reset_preferred_x(EditorState *ed) {
+ ed->preferredX = -1.0f;
+}
+
+/* Collapses the selection anchor onto the current cursor position (extend=false), or leaves
+ it untouched to extend the existing selection (extend=true). */
+static void snap_anchor_to_cursor(EditorState *ed, bool extend) {
+ if (extend) return;
+ ed->selAnchorBlock = ed->cursorBlock;
+ ed->selAnchorOffset = ed->cursorOffset;
+}
+
+void editor_init(EditorState *ed) {
+ document_init(&ed->doc);
+ ed->cursorBlock = 0;
+ ed->cursorOffset = 0;
+ ed->preferredX = -1.0f;
+ ed->filePath = NULL;
+ ed->dirty = false;
+ undo_manager_init(&ed->undo, 200);
+ ed->selAnchorBlock = 0;
+ ed->selAnchorOffset = 0;
+}
+
+void editor_free(EditorState *ed) {
+ document_free(&ed->doc);
+ free(ed->filePath);
+ ed->filePath = NULL;
+ undo_manager_free(&ed->undo);
+}
+
+bool editor_load(EditorState *ed, const char *path) {
+ free(ed->filePath);
+ size_t n = strlen(path);
+ ed->filePath = malloc(n + 1);
+ memcpy(ed->filePath, path, n + 1);
+
+ bool ok = document_load_file(&ed->doc, path);
+ ed->cursorBlock = 0;
+ ed->cursorOffset = 0;
+ ed->dirty = false;
+ undo_manager_reset(&ed->undo);
+ ed->selAnchorBlock = 0;
+ ed->selAnchorOffset = 0;
+ reset_preferred_x(ed);
+ return ok;
+}
+
+bool editor_save(EditorState *ed) {
+ if (!ed->filePath) return false;
+ bool ok = document_save_file(&ed->doc, ed->filePath);
+ if (ok) ed->dirty = false;
+ return ok;
+}
+
+const char *editor_display_name(const EditorState *ed) {
+ if (!ed->filePath) return "Untitled";
+ const char *slash = strrchr(ed->filePath, '/');
+ return slash ? slash + 1 : ed->filePath;
+}
+
+/* Converts a leading run of paragraph text (e.g. "#", "##", "-", "1.") typed just before the
+ space that was just inserted into the corresponding block type, WYSIWYG-style. */
+static void maybe_autoformat_block(EditorState *ed) {
+ Block *b = &ed->doc.blocks[ed->cursorBlock];
+ if (b->type != BLOCK_PARAGRAPH) return;
+
+ int upto = ed->cursorOffset - 1; /* text before the space just typed */
+ if (upto <= 0) return;
+ const char *t = b->text.data;
+
+ BlockType newType;
+ if (upto == 1 && t[0] == '#') newType = BLOCK_H1;
+ else if (upto == 2 && t[0] == '#' && t[1] == '#') newType = BLOCK_H2;
+ else if (upto == 3 && t[0] == '#' && t[1] == '#' && t[2] == '#') newType = BLOCK_H3;
+ else if (upto == 1 && (t[0] == '-' || t[0] == '*')) newType = BLOCK_BULLET;
+ else if (upto == 1 && t[0] == '>') newType = BLOCK_QUOTE;
+ else if (upto >= 2 && isdigit((unsigned char)t[0])) {
+ int i = 0;
+ while (i < upto && isdigit((unsigned char)t[i])) i++;
+ if (i < upto && t[i] == '.' && i + 1 == upto) newType = BLOCK_NUMBERED;
+ else return;
+ } else {
+ return;
+ }
+
+ sb_delete(&b->text, 0, ed->cursorOffset);
+ ed->cursorOffset = 0;
+ b->type = newType;
+}
+
+bool editor_has_selection(const EditorState *ed) {
+ return ed->selAnchorBlock != ed->cursorBlock || ed->selAnchorOffset != ed->cursorOffset;
+}
+
+void editor_selection_range(const EditorState *ed, int *startBlock, int *startOffset, int *endBlock, int *endOffset) {
+ int ab = ed->selAnchorBlock, ao = ed->selAnchorOffset, cb = ed->cursorBlock, co = ed->cursorOffset;
+ if (ab < cb || (ab == cb && ao <= co)) {
+ *startBlock = ab; *startOffset = ao; *endBlock = cb; *endOffset = co;
+ } else {
+ *startBlock = cb; *startOffset = co; *endBlock = ab; *endOffset = ao;
+ }
+}
+
+/* If a selection is active, deletes it (merging a cross-block selection into the start
+ block, which keeps its type) and collapses the cursor/anchor to the deletion point.
+ Returns whether there was a selection to delete. */
+static bool delete_selection_if_any(EditorState *ed) {
+ if (!editor_has_selection(ed)) return false;
+ int sb, so, eb, eo;
+ editor_selection_range(ed, &sb, &so, &eb, &eo);
+ if (sb == eb) {
+ sb_delete(&ed->doc.blocks[sb].text, so, eo - so);
+ } else {
+ Block *startBlock = &ed->doc.blocks[sb];
+ Block *endBlock = &ed->doc.blocks[eb];
+ sb_delete(&startBlock->text, so, startBlock->text.len - so);
+ sb_append(&startBlock->text, endBlock->text.data + eo, endBlock->text.len - eo);
+ for (int i = eb; i > sb; i--) document_remove_block(&ed->doc, i);
+ }
+ ed->cursorBlock = ed->selAnchorBlock = sb;
+ ed->cursorOffset = ed->selAnchorOffset = so;
+ ed->dirty = true;
+ reset_preferred_x(ed);
+ return true;
+}
+
+char *editor_selection_to_text(const EditorState *ed, int *outLen) {
+ if (!editor_has_selection(ed)) { if (outLen) *outLen = 0; return NULL; }
+ int sb, so, eb, eo;
+ editor_selection_range(ed, &sb, &so, &eb, &eo);
+ StrBuf out;
+ sb_init(&out);
+ for (int i = sb; i <= eb; i++) {
+ Block *b = &ed->doc.blocks[i];
+ int start = (i == sb) ? so : 0;
+ int end = (i == eb) ? eo : b->text.len;
+ if (i > sb) sb_append_char(&out, '\n');
+ sb_append(&out, b->text.data + start, end - start);
+ }
+ char *result = malloc((size_t)out.len + 1);
+ memcpy(result, out.data, (size_t)out.len);
+ result[out.len] = '\0';
+ if (outLen) *outLen = out.len;
+ sb_free(&out);
+ return result;
+}
+
+static void insert_utf8_raw(EditorState *ed, const char *bytes, int n) {
+ Block *b = &ed->doc.blocks[ed->cursorBlock];
+ sb_insert(&b->text, ed->cursorOffset, bytes, n);
+ ed->cursorOffset += n;
+ ed->dirty = true;
+ if (n == 1 && bytes[0] == ' ') maybe_autoformat_block(ed);
+ reset_preferred_x(ed);
+}
+
+void editor_insert_utf8(EditorState *ed, const char *bytes, int n) {
+ if (n <= 0) return;
+ undo_manager_before_edit(&ed->undo, EDIT_TYPE, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+ delete_selection_if_any(ed);
+ insert_utf8_raw(ed, bytes, n);
+ snap_anchor_to_cursor(ed, false);
+ undo_manager_after_edit(&ed->undo, EDIT_TYPE, ed->cursorBlock, ed->cursorOffset);
+}
+
+void editor_paste(EditorState *ed, const char *text, int len) {
+ if (len <= 0) return;
+ char *tmp = malloc((size_t)len);
+ int n = 0;
+ for (int i = 0; i < len; i++) {
+ char c = text[i];
+ if (c == '\r') continue;
+ if (c == '\n' || c == '\t') c = ' ';
+ if ((unsigned char)c < 0x20) continue;
+ tmp[n++] = c;
+ }
+ if (n > 0) {
+ undo_manager_before_edit(&ed->undo, EDIT_PASTE, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+ delete_selection_if_any(ed);
+ insert_utf8_raw(ed, tmp, n);
+ snap_anchor_to_cursor(ed, false);
+ undo_manager_after_edit(&ed->undo, EDIT_PASTE, ed->cursorBlock, ed->cursorOffset);
+ }
+ free(tmp);
+}
+
+static void backspace_raw(EditorState *ed) {
+ Block *b = &ed->doc.blocks[ed->cursorBlock];
+ if (ed->cursorOffset > 0) {
+ int prevStart = utf8_prev_start(b->text.data, ed->cursorOffset);
+ sb_delete(&b->text, prevStart, ed->cursorOffset - prevStart);
+ ed->cursorOffset = prevStart;
+ ed->dirty = true;
+ } else if (b->type != BLOCK_PARAGRAPH) {
+ if (b->type == BLOCK_CODE) {
+ for (int i = 0; i < b->text.len; i++) if (b->text.data[i] == '\n') b->text.data[i] = ' ';
+ }
+ b->type = BLOCK_PARAGRAPH;
+ ed->dirty = true;
+ } else if (ed->cursorBlock > 0) {
+ Block *prev = &ed->doc.blocks[ed->cursorBlock - 1];
+ int mergeAt = prev->text.len;
+ sb_append(&prev->text, b->text.data, b->text.len);
+ document_remove_block(&ed->doc, ed->cursorBlock);
+ ed->cursorBlock--;
+ ed->cursorOffset = mergeAt;
+ ed->dirty = true;
+ }
+ reset_preferred_x(ed);
+}
+
+void editor_backspace(EditorState *ed) {
+ undo_manager_before_edit(&ed->undo, EDIT_BACKSPACE, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+ if (delete_selection_if_any(ed)) {
+ undo_manager_after_edit(&ed->undo, EDIT_BACKSPACE, ed->cursorBlock, ed->cursorOffset);
+ return;
+ }
+ backspace_raw(ed);
+ snap_anchor_to_cursor(ed, false);
+ undo_manager_after_edit(&ed->undo, EDIT_BACKSPACE, ed->cursorBlock, ed->cursorOffset);
+}
+
+static void delete_forward_raw(EditorState *ed) {
+ Block *b = &ed->doc.blocks[ed->cursorBlock];
+ if (ed->cursorOffset < b->text.len) {
+ int nlen = utf8_next_len(b->text.data, ed->cursorOffset, b->text.len);
+ sb_delete(&b->text, ed->cursorOffset, nlen);
+ ed->dirty = true;
+ } else if (ed->cursorBlock < ed->doc.count - 1) {
+ Block *next = &ed->doc.blocks[ed->cursorBlock + 1];
+ if (b->type == BLOCK_CODE && next->type != BLOCK_CODE) {
+ sb_append_char(&b->text, '\n');
+ } else if (next->type == BLOCK_CODE && b->type != BLOCK_CODE) {
+ for (int i = 0; i < next->text.len; i++) if (next->text.data[i] == '\n') next->text.data[i] = ' ';
+ }
+ sb_append(&b->text, next->text.data, next->text.len);
+ document_remove_block(&ed->doc, ed->cursorBlock + 1);
+ ed->dirty = true;
+ }
+ reset_preferred_x(ed);
+}
+
+void editor_delete_forward(EditorState *ed) {
+ undo_manager_before_edit(&ed->undo, EDIT_DELETE_FWD, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+ if (delete_selection_if_any(ed)) {
+ undo_manager_after_edit(&ed->undo, EDIT_DELETE_FWD, ed->cursorBlock, ed->cursorOffset);
+ return;
+ }
+ delete_forward_raw(ed);
+ snap_anchor_to_cursor(ed, false);
+ undo_manager_after_edit(&ed->undo, EDIT_DELETE_FWD, ed->cursorBlock, ed->cursorOffset);
+}
+
+static void enter_raw(EditorState *ed) {
+ Block *b = &ed->doc.blocks[ed->cursorBlock];
+
+ if (b->type == BLOCK_CODE) {
+ int lineStart = ed->cursorOffset;
+ while (lineStart > 0 && b->text.data[lineStart - 1] != '\n') lineStart--;
+ int lineEnd = ed->cursorOffset;
+ while (lineEnd < b->text.len && b->text.data[lineEnd] != '\n') lineEnd++;
+
+ if (lineEnd - lineStart == 3 && memcmp(b->text.data + lineStart, "```", 3) == 0) {
+ int removeFrom = lineStart > 0 ? lineStart - 1 : lineStart;
+ sb_delete(&b->text, removeFrom, b->text.len - removeFrom);
+ document_insert_block(&ed->doc, ed->cursorBlock + 1, BLOCK_PARAGRAPH, "", 0);
+ ed->cursorBlock++;
+ ed->cursorOffset = 0;
+ ed->dirty = true;
+ reset_preferred_x(ed);
+ return;
+ }
+
+ sb_insert(&b->text, ed->cursorOffset, "\n", 1);
+ ed->cursorOffset++;
+ ed->dirty = true;
+ reset_preferred_x(ed);
+ return;
+ }
+
+ if (b->type == BLOCK_PARAGRAPH && b->text.len == 3 && memcmp(b->text.data, "```", 3) == 0) {
+ b->type = BLOCK_CODE;
+ sb_clear(&b->text);
+ ed->cursorOffset = 0;
+ ed->dirty = true;
+ reset_preferred_x(ed);
+ return;
+ }
+
+ if (block_type_is_continuable(b->type) && b->text.len == 0) {
+ b->type = BLOCK_PARAGRAPH;
+ ed->dirty = true;
+ reset_preferred_x(ed);
+ return;
+ }
+
+ int splitAt = ed->cursorOffset;
+ int tailLen = b->text.len - splitAt;
+ BlockType newType = block_type_is_continuable(b->type) ? b->type : BLOCK_PARAGRAPH;
+
+ document_insert_block(&ed->doc, ed->cursorBlock + 1, newType, b->text.data + splitAt, tailLen);
+ b = &ed->doc.blocks[ed->cursorBlock]; /* doc.blocks may have been reallocated */
+ sb_delete(&b->text, splitAt, tailLen);
+
+ ed->cursorBlock++;
+ ed->cursorOffset = 0;
+ ed->dirty = true;
+ reset_preferred_x(ed);
+}
+
+void editor_enter(EditorState *ed) {
+ undo_manager_before_edit(&ed->undo, EDIT_ENTER, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+ delete_selection_if_any(ed);
+ enter_raw(ed);
+ snap_anchor_to_cursor(ed, false);
+ undo_manager_after_edit(&ed->undo, EDIT_ENTER, ed->cursorBlock, ed->cursorOffset);
+}
+
+void editor_move_left(EditorState *ed, bool extend) {
+ if (ed->cursorOffset > 0) {
+ Block *b = &ed->doc.blocks[ed->cursorBlock];
+ ed->cursorOffset = utf8_prev_start(b->text.data, ed->cursorOffset);
+ } else if (ed->cursorBlock > 0) {
+ ed->cursorBlock--;
+ ed->cursorOffset = ed->doc.blocks[ed->cursorBlock].text.len;
+ }
+ snap_anchor_to_cursor(ed, extend);
+ reset_preferred_x(ed);
+}
+
+void editor_move_right(EditorState *ed, bool extend) {
+ Block *b = &ed->doc.blocks[ed->cursorBlock];
+ if (ed->cursorOffset < b->text.len) {
+ ed->cursorOffset += utf8_next_len(b->text.data, ed->cursorOffset, b->text.len);
+ } else if (ed->cursorBlock < ed->doc.count - 1) {
+ ed->cursorBlock++;
+ ed->cursorOffset = 0;
+ }
+ snap_anchor_to_cursor(ed, extend);
+ reset_preferred_x(ed);
+}
+
+void editor_move_home(EditorState *ed, bool extend) {
+ ed->cursorOffset = 0;
+ snap_anchor_to_cursor(ed, extend);
+ reset_preferred_x(ed);
+}
+
+void editor_move_end(EditorState *ed, bool extend) {
+ ed->cursorOffset = ed->doc.blocks[ed->cursorBlock].text.len;
+ snap_anchor_to_cursor(ed, extend);
+ reset_preferred_x(ed);
+}
+
+void editor_move_vertical(EditorState *ed, LayoutCache *cache, Font *fonts, int dir, bool extend) {
+ if (ed->preferredX < 0.0f) {
+ float x, y, h;
+ if (hittest_caret(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, &x, &y, &h)) {
+ ed->preferredX = x;
+ } else {
+ ed->preferredX = 0.0f;
+ }
+ }
+ VerticalMoveResult r = hittest_vertical(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, ed->preferredX, dir);
+ if (r.found) {
+ ed->cursorBlock = r.blockIndex;
+ ed->cursorOffset = r.offset;
+ }
+ snap_anchor_to_cursor(ed, extend);
+}
+
+void editor_click(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point) {
+ HitResult r = hittest_point(cache, &ed->doc, fonts, point);
+ ed->cursorBlock = r.blockIndex;
+ ed->cursorOffset = r.offset;
+ ed->selAnchorBlock = r.blockIndex;
+ ed->selAnchorOffset = r.offset;
+ reset_preferred_x(ed);
+}
+
+void editor_drag_to(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point) {
+ HitResult r = hittest_point(cache, &ed->doc, fonts, point);
+ ed->cursorBlock = r.blockIndex;
+ ed->cursorOffset = r.offset;
+ reset_preferred_x(ed);
+}
+
+bool editor_undo(EditorState *ed) {
+ Snapshot s;
+ if (!snapshot_stack_pop(&ed->undo.undo, &s)) return false;
+ snapshot_stack_push(&ed->undo.redo, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+ document_free(&ed->doc);
+ ed->doc = s.doc;
+ ed->cursorBlock = s.cursorBlock;
+ ed->cursorOffset = s.cursorOffset;
+ ed->selAnchorBlock = s.cursorBlock;
+ ed->selAnchorOffset = s.cursorOffset;
+ ed->dirty = true;
+ ed->undo.lastKind = EDIT_NONE;
+ reset_preferred_x(ed);
+ return true;
+}
+
+bool editor_redo(EditorState *ed) {
+ Snapshot s;
+ if (!snapshot_stack_pop(&ed->undo.redo, &s)) return false;
+ snapshot_stack_push(&ed->undo.undo, &ed->doc, ed->cursorBlock, ed->cursorOffset);
+ document_free(&ed->doc);
+ ed->doc = s.doc;
+ ed->cursorBlock = s.cursorBlock;
+ ed->cursorOffset = s.cursorOffset;
+ ed->selAnchorBlock = s.cursorBlock;
+ ed->selAnchorOffset = s.cursorOffset;
+ ed->dirty = true;
+ ed->undo.lastKind = EDIT_NONE;
+ reset_preferred_x(ed);
+ return true;
+}
diff --git a/src/editor.h b/src/editor.h
new file mode 100644
index 0000000..e4eac2e
--- /dev/null
+++ b/src/editor.h
@@ -0,0 +1,63 @@
+#ifndef HUSH_EDITOR_H
+#define HUSH_EDITOR_H
+
+#include "document.h"
+#include "layout_cache.h"
+#include "undo.h"
+#include "raylib.h"
+#include <stdbool.h>
+
+typedef struct {
+ Document doc;
+ int cursorBlock;
+ int cursorOffset;
+ float preferredX; /* sticky x used across consecutive up/down moves; <0 means "recompute" */
+ char *filePath; /* owned, may be NULL if the editor was started without a file */
+ bool dirty;
+ UndoManager undo;
+ int selAnchorBlock, selAnchorOffset; /* selection anchor; anchor == cursor means "no selection" */
+} EditorState;
+
+void editor_init(EditorState *ed);
+void editor_free(EditorState *ed);
+
+/* Loads `path` into the editor, remembering it for future saves regardless of success
+ (so Ctrl+S can create a brand new file). Returns false if the file could not be read. */
+bool editor_load(EditorState *ed, const char *path);
+bool editor_save(EditorState *ed);
+
+/* Basename of ed->filePath, or "Untitled" if the editor has no associated file yet. */
+const char *editor_display_name(const EditorState *ed);
+
+void editor_insert_utf8(EditorState *ed, const char *bytes, int n);
+void editor_paste(EditorState *ed, const char *text, int len);
+void editor_backspace(EditorState *ed);
+void editor_delete_forward(EditorState *ed);
+void editor_enter(EditorState *ed);
+
+bool editor_undo(EditorState *ed);
+bool editor_redo(EditorState *ed);
+
+/* `extend`: false moves the caret and collapses/resets the selection anchor to it (plain
+ movement); true moves only the caret, extending the selection from the existing anchor
+ (Shift+movement). */
+void editor_move_left(EditorState *ed, bool extend);
+void editor_move_right(EditorState *ed, bool extend);
+void editor_move_home(EditorState *ed, bool extend);
+void editor_move_end(EditorState *ed, bool extend);
+void editor_move_vertical(EditorState *ed, LayoutCache *cache, Font *fonts, int dir, bool extend);
+
+/* Mouse press: moves the caret and (re)establishes the selection anchor there. */
+void editor_click(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point);
+/* Mouse drag (button held after the initial press): moves only the caret, extending the
+ selection from whatever editor_click most recently set as the anchor. */
+void editor_drag_to(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point);
+
+bool editor_has_selection(const EditorState *ed);
+/* Normalizes the anchor/cursor pair into forward document order. */
+void editor_selection_range(const EditorState *ed, int *startBlock, int *startOffset, int *endBlock, int *endOffset);
+/* Returns the selected text (blocks joined with '\n'), or NULL if there is no selection.
+ Caller must free() the result. */
+char *editor_selection_to_text(const EditorState *ed, int *outLen);
+
+#endif
diff --git a/src/fonts.c b/src/fonts.c
new file mode 100644
index 0000000..b158e52
--- /dev/null
+++ b/src/fonts.c
@@ -0,0 +1,61 @@
+#include "fonts.h"
+#include <stdlib.h>
+
+#define FONT_BASE_SIZE 48
+
+/* ASCII + Latin-1 supplement, so accented characters (e.g. Swedish å ä ö) render correctly,
+ plus a couple of symbols used in the UI that fall outside that range. */
+static int *build_codepoints(int *count) {
+ int latin1 = 255 - 32 + 1;
+ static const int extra[] = {
+ 0x2022, /* bullet, used for BLOCK_BULLET markers */
+ 0x2014, /* em dash, used in the status bar and modal UI text */
+ };
+ int extraCount = (int)(sizeof(extra) / sizeof(extra[0]));
+ int n = latin1 + extraCount;
+
+ int *cps = malloc((size_t)n * sizeof(int));
+ for (int i = 0; i < latin1; i++) cps[i] = 32 + i;
+ for (int i = 0; i < extraCount; i++) cps[latin1 + i] = extra[i];
+
+ *count = n;
+ return cps;
+}
+
+static void load_one(Font *font, const char *overridePath, const char *defaultPath, const int *codepoints, int codepointCount) {
+ const char *path = (overridePath && overridePath[0]) ? overridePath : defaultPath;
+ *font = LoadFontEx(path, FONT_BASE_SIZE, (int *)codepoints, codepointCount);
+ if (font->texture.id == 0 && overridePath) {
+ /* Bad override path (e.g. a typo) -- fall back to the bundled default rather than
+ leaving this style slot with no usable font. */
+ *font = LoadFontEx(defaultPath, FONT_BASE_SIZE, (int *)codepoints, codepointCount);
+ }
+ if (font->texture.id != 0) {
+ /* Glyphs are drawn well below FONT_BASE_SIZE (see theme.h's FS_* sizes), so without
+ mipmaps, plain bilinear minification aliases badly ("wrong resolution" look). */
+ GenTextureMipmaps(&font->texture);
+ SetTextureFilter(font->texture, TEXTURE_FILTER_TRILINEAR);
+ }
+}
+
+void fonts_load(Font *fonts, char *const *overridePaths) {
+ int codepointCount;
+ int *codepoints = build_codepoints(&codepointCount);
+ char *none[FONT_COUNT] = {0};
+ char *const *ov = overridePaths ? overridePaths : none;
+
+ load_one(&fonts[FONT_SANS_REGULAR], ov[FONT_SANS_REGULAR], ASSETS_DIR "/fonts/DejaVuSans.ttf", codepoints, codepointCount);
+ load_one(&fonts[FONT_SANS_BOLD], ov[FONT_SANS_BOLD], ASSETS_DIR "/fonts/DejaVuSans-Bold.ttf", codepoints, codepointCount);
+ load_one(&fonts[FONT_SANS_ITALIC], ov[FONT_SANS_ITALIC], ASSETS_DIR "/fonts/DejaVuSans-Oblique.ttf", codepoints, codepointCount);
+ load_one(&fonts[FONT_SANS_BOLD_ITALIC], ov[FONT_SANS_BOLD_ITALIC], ASSETS_DIR "/fonts/DejaVuSans-BoldOblique.ttf", codepoints, codepointCount);
+ load_one(&fonts[FONT_MONO_REGULAR], ov[FONT_MONO_REGULAR], ASSETS_DIR "/fonts/DejaVuSansMono.ttf", codepoints, codepointCount);
+ load_one(&fonts[FONT_MONO_BOLD], ov[FONT_MONO_BOLD], ASSETS_DIR "/fonts/DejaVuSansMono-Bold.ttf", codepoints, codepointCount);
+ load_one(&fonts[FONT_MONO_ITALIC], ov[FONT_MONO_ITALIC], ASSETS_DIR "/fonts/DejaVuSansMono-Oblique.ttf", codepoints, codepointCount);
+ load_one(&fonts[FONT_MONO_BOLD_ITALIC], ov[FONT_MONO_BOLD_ITALIC], ASSETS_DIR "/fonts/DejaVuSansMono-BoldOblique.ttf", codepoints, codepointCount);
+
+ free(codepoints);
+}
+
+void fonts_unload(Font *fonts) {
+ for (int i = 0; i < FONT_COUNT; i++) UnloadFont(fonts[i]);
+}
diff --git a/src/fonts.h b/src/fonts.h
new file mode 100644
index 0000000..6b352f6
--- /dev/null
+++ b/src/fonts.h
@@ -0,0 +1,28 @@
+#ifndef HUSH_FONTS_H
+#define HUSH_FONTS_H
+
+#include "raylib.h"
+
+#define FONT_SANS_REGULAR 0
+#define FONT_SANS_BOLD 1
+#define FONT_SANS_ITALIC 2
+#define FONT_SANS_BOLD_ITALIC 3
+#define FONT_MONO_REGULAR 4
+#define FONT_MONO_BOLD 5
+#define FONT_MONO_ITALIC 6
+#define FONT_MONO_BOLD_ITALIC 7
+#define FONT_COUNT 8
+
+/* fonts[] must have room for FONT_COUNT entries. Loaded at a large base size so that
+ scaling down for on-screen font sizes stays sharp. `overridePaths[i]`, if non-NULL,
+ replaces the bundled default for that slot (see font_index/FONT_* above); pass NULL for
+ the whole array to use only the bundled defaults. A bad override path falls back to the
+ bundled default rather than leaving an unusable font. */
+void fonts_load(Font *fonts, char *const *overridePaths);
+void fonts_unload(Font *fonts);
+
+static inline int font_index(int mono, int bold, int italic) {
+ return (mono ? FONT_MONO_REGULAR : FONT_SANS_REGULAR) + (bold ? 1 : 0) + (italic ? 2 : 0);
+}
+
+#endif
diff --git a/src/hittest.c b/src/hittest.c
new file mode 100644
index 0000000..7c732d4
--- /dev/null
+++ b/src/hittest.c
@@ -0,0 +1,176 @@
+#include "hittest.h"
+#include "clay.h"
+#include "utf8.h"
+#include <string.h>
+#include <float.h>
+
+#define MEASURE_BUF_SIZE 2048
+
+static float measure_width(Font *fonts, int fontId, int fontSize, const char *text, int start, int n) {
+ if (n <= 0) return 0.0f;
+ char buf[MEASURE_BUF_SIZE];
+ if (n >= MEASURE_BUF_SIZE) n = MEASURE_BUF_SIZE - 1;
+ memcpy(buf, text + start, (size_t)n);
+ buf[n] = '\0';
+ Vector2 sz = MeasureTextEx(fonts[fontId], buf, (float)fontSize, 0.0f);
+ return sz.x;
+}
+
+static Clay_ElementData get_line_data(int globalLineIndex) {
+ return Clay_GetElementData(CLAY_IDI("Line", globalLineIndex));
+}
+
+static Clay_ElementData get_seg_data(int globalSegIndex) {
+ return Clay_GetElementData(CLAY_IDI("Seg", globalSegIndex));
+}
+
+/* Finds the array index (into cache->lines) of the line that should own `offset` within `blockIndex`.
+ When `offset` sits exactly on the boundary between two lines, prefers the later one (start of the
+ next visual line), matching how a caret naturally rests after a wrapped word. */
+static int find_line_for_offset(LayoutCache *cache, int blockIndex, int offset) {
+ int best = -1;
+ for (int i = 0; i < cache->lineCount; i++) {
+ CachedLine *l = &cache->lines[i];
+ if (l->blockIndex != blockIndex) continue;
+ if (l->srcStart <= offset) best = i;
+ }
+ return best;
+}
+
+/* Finds the byte offset within `lineIdx` whose glyph is closest to screen-space `targetX`. */
+static int find_offset_in_line(LayoutCache *cache, Document *doc, Font *fonts, int lineIdx, float targetX) {
+ CachedLine *line = &cache->lines[lineIdx];
+ if (line->segStart == line->segEnd) return line->srcStart;
+
+ const char *text = doc->blocks[line->blockIndex].text.data;
+
+ int bestSeg = -1;
+ Clay_BoundingBox bestBox = {0};
+ for (int i = line->segStart; i < line->segEnd; i++) {
+ Clay_ElementData d = get_seg_data(cache->segs[i].globalSegIndex);
+ if (!d.found) continue;
+ if (bestSeg < 0) { bestSeg = i; bestBox = d.boundingBox; }
+ if (targetX >= d.boundingBox.x && targetX < d.boundingBox.x + d.boundingBox.width) {
+ bestSeg = i;
+ bestBox = d.boundingBox;
+ break;
+ }
+ if (targetX >= d.boundingBox.x) { bestSeg = i; bestBox = d.boundingBox; }
+ }
+ if (bestSeg < 0) return line->srcStart;
+
+ CachedSegment *seg = &cache->segs[bestSeg];
+ float localTarget = targetX - bestBox.x;
+ if (localTarget <= 0.0f) return seg->srcStart;
+
+ int pos = seg->srcStart;
+ while (pos < seg->srcEnd) {
+ int clen = utf8_next_len(text, pos, seg->srcEnd);
+ float wBefore = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, pos - seg->srcStart);
+ float wAfter = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, pos + clen - seg->srcStart);
+ if (wAfter >= localTarget) {
+ float mid = (wBefore + wAfter) * 0.5f;
+ return (localTarget < mid) ? pos : pos + clen;
+ }
+ pos += clen;
+ }
+ return seg->srcEnd;
+}
+
+HitResult hittest_point(LayoutCache *cache, Document *doc, Font *fonts, Vector2 point) {
+ HitResult r = {0, 0};
+ if (cache->lineCount == 0 || doc->count == 0) return r;
+
+ int bestLine = -1;
+ float bestDist = FLT_MAX;
+ for (int i = 0; i < cache->lineCount; i++) {
+ Clay_ElementData d = get_line_data(cache->lines[i].globalLineIndex);
+ if (!d.found) continue;
+ float top = d.boundingBox.y, bot = top + d.boundingBox.height;
+ if (point.y >= top && point.y < bot) { bestLine = i; bestDist = 0.0f; break; }
+ float dist = point.y < top ? (top - point.y) : (point.y - bot);
+ if (dist < bestDist) { bestDist = dist; bestLine = i; }
+ }
+ if (bestLine < 0) {
+ r.blockIndex = doc->count - 1;
+ r.offset = doc->blocks[r.blockIndex].text.len;
+ return r;
+ }
+
+ r.blockIndex = cache->lines[bestLine].blockIndex;
+ r.offset = find_offset_in_line(cache, doc, fonts, bestLine, point.x);
+ return r;
+}
+
+VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float preferredX, int dir) {
+ VerticalMoveResult r = {0, 0, 0};
+ int curLine = find_line_for_offset(cache, blockIndex, offset);
+ if (curLine < 0) return r;
+ int targetLine = curLine + (dir < 0 ? -1 : 1);
+ if (targetLine < 0 || targetLine >= cache->lineCount) return r;
+
+ r.found = 1;
+ r.blockIndex = cache->lines[targetLine].blockIndex;
+ r.offset = find_offset_in_line(cache, doc, fonts, targetLine, preferredX);
+ return r;
+}
+
+/* Screen-space x of byte `offset` within visual line `line` (whose Clay element data,
+ `lineData`, the caller already has). Shared by hittest_caret and hittest_line_range_box. */
+static float x_at_offset_in_line(LayoutCache *cache, Document *doc, Font *fonts, CachedLine *line, Clay_ElementData lineData, int offset) {
+ if (line->segStart == line->segEnd) return lineData.boundingBox.x;
+
+ int bestSeg = -1;
+ for (int i = line->segStart; i < line->segEnd; i++) {
+ if (cache->segs[i].srcStart <= offset) bestSeg = i;
+ }
+ if (bestSeg < 0) bestSeg = line->segStart;
+
+ CachedSegment *seg = &cache->segs[bestSeg];
+ Clay_ElementData sd = get_seg_data(seg->globalSegIndex);
+ if (!sd.found) return lineData.boundingBox.x;
+
+ int within = offset - seg->srcStart;
+ if (within < 0) within = 0;
+ if (within > seg->srcEnd - seg->srcStart) within = seg->srcEnd - seg->srcStart;
+
+ const char *text = doc->blocks[line->blockIndex].text.data;
+ float w = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, within);
+ return sd.boundingBox.x + w;
+}
+
+int hittest_caret(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight) {
+ int lineIdx = find_line_for_offset(cache, blockIndex, offset);
+ if (lineIdx < 0) return 0;
+ CachedLine *line = &cache->lines[lineIdx];
+ Clay_ElementData ld = get_line_data(line->globalLineIndex);
+ if (!ld.found) return 0;
+
+ *outY = ld.boundingBox.y;
+ *outHeight = ld.boundingBox.height;
+ *outX = x_at_offset_in_line(cache, doc, fonts, line, ld, offset);
+ return 1;
+}
+
+int hittest_line_range_box(LayoutCache *cache, Document *doc, Font *fonts, int lineIdx,
+ int rangeStart, int rangeEnd, float *outX, float *outY, float *outW, float *outHeight) {
+ if (lineIdx < 0 || lineIdx >= cache->lineCount) return 0;
+ CachedLine *line = &cache->lines[lineIdx];
+ int lo = rangeStart < line->srcStart ? line->srcStart : rangeStart;
+ int hi = rangeEnd > line->srcEnd ? line->srcEnd : rangeEnd;
+ if (hi < lo) return 0;
+
+ Clay_ElementData ld = get_line_data(line->globalLineIndex);
+ if (!ld.found) return 0;
+
+ float x0 = x_at_offset_in_line(cache, doc, fonts, line, ld, lo);
+ float x1 = x_at_offset_in_line(cache, doc, fonts, line, ld, hi);
+ float w = x1 - x0;
+ if (w < 4.0f) w = 4.0f; /* visible sliver for a fully-selected blank line */
+
+ *outX = x0;
+ *outY = ld.boundingBox.y;
+ *outW = w;
+ *outHeight = ld.boundingBox.height;
+ return 1;
+}
diff --git a/src/hittest.h b/src/hittest.h
new file mode 100644
index 0000000..571f24e
--- /dev/null
+++ b/src/hittest.h
@@ -0,0 +1,38 @@
+#ifndef HUSH_HITTEST_H
+#define HUSH_HITTEST_H
+
+#include "layout_cache.h"
+#include "document.h"
+#include "raylib.h"
+
+typedef struct {
+ int blockIndex;
+ int offset;
+} HitResult;
+
+/* Maps a screen-space point to the nearest (block, byte offset), using the Clay element
+ positions computed for the layout built during the previous frame. */
+HitResult hittest_point(LayoutCache *cache, Document *doc, Font *fonts, Vector2 point);
+
+typedef struct {
+ int found;
+ int blockIndex;
+ int offset;
+} VerticalMoveResult;
+
+/* Moves the cursor to the visual line above (dir<0) or below (dir>0), trying to land as
+ close as possible to `preferredX` (screen-space x). */
+VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float preferredX, int dir);
+
+/* Fills the caret's screen box for (blockIndex, offset). Returns 0 if the layout has no
+ matching entry yet (e.g. very first frame). */
+int hittest_caret(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight);
+
+/* Fills the screen-space box covering [rangeStart, rangeEnd) (byte offsets within the line's
+ block) as visible on visual line `lineIdx`, clamped to that line's own byte range. Returns 0
+ if the range doesn't intersect the line or the layout has no matching entry yet. Used to draw
+ the selection highlight, one rectangle per intersected visual line. */
+int hittest_line_range_box(LayoutCache *cache, Document *doc, Font *fonts, int lineIdx,
+ int rangeStart, int rangeEnd, float *outX, float *outY, float *outW, float *outHeight);
+
+#endif
diff --git a/src/inline_parse.c b/src/inline_parse.c
new file mode 100644
index 0000000..1d30b11
--- /dev/null
+++ b/src/inline_parse.c
@@ -0,0 +1,117 @@
+#include "inline_parse.h"
+#include <stdlib.h>
+
+void inline_runs_init(InlineRunList *list) {
+ list->runs = NULL;
+ list->count = 0;
+ list->cap = 0;
+}
+
+void inline_runs_free(InlineRunList *list) {
+ free(list->runs);
+ list->runs = NULL;
+ list->count = 0;
+ list->cap = 0;
+}
+
+static void push_run(InlineRunList *list, RunKind kind, int rawStart, int rawEnd, int contentStart, int contentEnd) {
+ if (list->count >= list->cap) {
+ list->cap = list->cap > 0 ? list->cap * 2 : 8;
+ list->runs = realloc(list->runs, (size_t)list->cap * sizeof(InlineRun));
+ }
+ InlineRun *r = &list->runs[list->count++];
+ r->kind = kind;
+ r->rawStart = rawStart;
+ r->rawEnd = rawEnd;
+ r->contentStart = contentStart;
+ r->contentEnd = contentEnd;
+}
+
+/* Finds the next unescaped occurrence of `c`, starting at `from`. -1 if not found. */
+static int find_char(const char *text, int len, int from, char c) {
+ for (int i = from; i < len; i++) {
+ if (text[i] == '\\' && i + 1 < len) { i++; continue; }
+ if (text[i] == c) return i;
+ }
+ return -1;
+}
+
+/* Finds the next unescaped occurrence of the two-char delimiter `cc`. -1 if not found. */
+static int find_double(const char *text, int len, int from, char c) {
+ for (int i = from; i < len - 1; i++) {
+ if (text[i] == '\\' && i + 1 < len) { i++; continue; }
+ if (text[i] == c && text[i + 1] == c) return i;
+ }
+ return -1;
+}
+
+void inline_parse(const char *text, int len, InlineRunList *out) {
+ inline_runs_init(out);
+ int i = 0;
+ int plainStart = 0;
+
+ while (i < len) {
+ char c = text[i];
+ int matched = 0;
+
+ if (c == '`') {
+ int j = find_char(text, len, i + 1, '`');
+ if (j >= 0 && j > i + 1) {
+ if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+ push_run(out, RUN_CODE, i, j + 1, i + 1, j);
+ i = j + 1;
+ plainStart = i;
+ matched = 1;
+ }
+ } else if ((c == '*' || c == '_') && i + 2 < len && text[i + 1] == c && text[i + 2] == c) {
+ int j = -1;
+ for (int k = i + 3; k < len - 2; k++) {
+ if (text[k] == c && text[k + 1] == c && text[k + 2] == c) { j = k; break; }
+ }
+ if (j >= 0 && j > i + 3) {
+ if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+ push_run(out, RUN_BOLD_ITALIC, i, j + 3, i + 3, j);
+ i = j + 3;
+ plainStart = i;
+ matched = 1;
+ }
+ } else if ((c == '*' || c == '_') && i + 1 < len && text[i + 1] == c) {
+ int j = find_double(text, len, i + 2, c);
+ if (j >= 0 && j > i + 2) {
+ if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+ push_run(out, RUN_BOLD, i, j + 2, i + 2, j);
+ i = j + 2;
+ plainStart = i;
+ matched = 1;
+ }
+ } else if (c == '*' || c == '_') {
+ int j = find_char(text, len, i + 1, c);
+ if (j >= 0 && j > i + 1) {
+ if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+ push_run(out, RUN_ITALIC, i, j + 1, i + 1, j);
+ i = j + 1;
+ plainStart = i;
+ matched = 1;
+ }
+ } else if (c == '[') {
+ int closeBracket = find_char(text, len, i + 1, ']');
+ if (closeBracket >= 0 && closeBracket > i + 1 && closeBracket + 1 < len && text[closeBracket + 1] == '(') {
+ int closeParen = find_char(text, len, closeBracket + 2, ')');
+ if (closeParen >= 0) {
+ if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
+ push_run(out, RUN_LINK, i, closeParen + 1, i + 1, closeBracket);
+ i = closeParen + 1;
+ plainStart = i;
+ matched = 1;
+ }
+ }
+ } else if (c == '\\' && i + 1 < len) {
+ i += 2;
+ continue;
+ }
+
+ if (!matched) i++;
+ }
+
+ if (len > plainStart) push_run(out, RUN_PLAIN, plainStart, len, plainStart, len);
+}
diff --git a/src/inline_parse.h b/src/inline_parse.h
new file mode 100644
index 0000000..5583059
--- /dev/null
+++ b/src/inline_parse.h
@@ -0,0 +1,32 @@
+#ifndef HUSH_INLINE_PARSE_H
+#define HUSH_INLINE_PARSE_H
+
+typedef enum {
+ RUN_PLAIN,
+ RUN_BOLD,
+ RUN_ITALIC,
+ RUN_BOLD_ITALIC,
+ RUN_CODE,
+ RUN_LINK,
+} RunKind;
+
+typedef struct {
+ RunKind kind;
+ /* Byte range in the block's raw text, INCLUDING markdown markers (e.g. "**bold**"). */
+ int rawStart, rawEnd;
+ /* Byte range of the actual displayable content, EXCLUDING markers (e.g. "bold"). */
+ int contentStart, contentEnd;
+} InlineRun;
+
+typedef struct {
+ InlineRun *runs;
+ int count, cap;
+} InlineRunList;
+
+void inline_runs_init(InlineRunList *list);
+void inline_runs_free(InlineRunList *list);
+
+/* Parses `text[0..len)` into a sequence of runs covering the whole range with no gaps. */
+void inline_parse(const char *text, int len, InlineRunList *out);
+
+#endif
diff --git a/src/layout_cache.c b/src/layout_cache.c
new file mode 100644
index 0000000..fa5fe08
--- /dev/null
+++ b/src/layout_cache.c
@@ -0,0 +1,55 @@
+#include "layout_cache.h"
+#include <stdlib.h>
+
+void layout_cache_init(LayoutCache *c) {
+ c->lines = NULL; c->lineCount = 0; c->lineCap = 0;
+ c->segs = NULL; c->segCount = 0; c->segCap = 0;
+}
+
+void layout_cache_reset(LayoutCache *c) {
+ c->lineCount = 0;
+ c->segCount = 0;
+}
+
+void layout_cache_free(LayoutCache *c) {
+ free(c->lines);
+ free(c->segs);
+ layout_cache_init(c);
+}
+
+int layout_cache_add_line(LayoutCache *c, int blockIndex, int lineInBlock, int srcStart, int srcEnd) {
+ if (c->lineCount >= c->lineCap) {
+ c->lineCap = c->lineCap > 0 ? c->lineCap * 2 : 64;
+ c->lines = realloc(c->lines, (size_t)c->lineCap * sizeof(CachedLine));
+ }
+ int idx = c->lineCount++;
+ CachedLine *l = &c->lines[idx];
+ l->blockIndex = blockIndex;
+ l->lineInBlock = lineInBlock;
+ l->srcStart = srcStart;
+ l->srcEnd = srcEnd;
+ l->globalLineIndex = idx;
+ l->segStart = c->segCount;
+ l->segEnd = c->segCount;
+ return idx;
+}
+
+int layout_cache_add_seg(LayoutCache *c, int lineIndex, int srcStart, int srcEnd, int fontId, int fontSize) {
+ if (c->segCount >= c->segCap) {
+ c->segCap = c->segCap > 0 ? c->segCap * 2 : 128;
+ c->segs = realloc(c->segs, (size_t)c->segCap * sizeof(CachedSegment));
+ }
+ int idx = c->segCount++;
+ CachedSegment *s = &c->segs[idx];
+ s->lineIndex = lineIndex;
+ s->srcStart = srcStart;
+ s->srcEnd = srcEnd;
+ s->globalSegIndex = idx;
+ s->fontId = fontId;
+ s->fontSize = fontSize;
+ return idx;
+}
+
+void layout_cache_close_line(LayoutCache *c, int lineIndex) {
+ c->lines[lineIndex].segEnd = c->segCount;
+}
diff --git a/src/layout_cache.h b/src/layout_cache.h
new file mode 100644
index 0000000..d1a5e6e
--- /dev/null
+++ b/src/layout_cache.h
@@ -0,0 +1,38 @@
+#ifndef HUSH_LAYOUT_CACHE_H
+#define HUSH_LAYOUT_CACHE_H
+
+/* A rendered visual line, produced fresh every frame while building the Clay tree.
+ Used the *following* frame to resolve clicks and up/down cursor movement against
+ real, already-computed Clay element positions (queried via Clay_GetElementData). */
+typedef struct {
+ int blockIndex;
+ int lineInBlock;
+ int srcStart, srcEnd; /* byte range in the block's raw text covered by this line */
+ int globalLineIndex; /* identifies this line's Clay element: CLAY_IDI("Line", globalLineIndex) */
+ int segStart, segEnd; /* range into LayoutCache.segs covering this line's segments */
+} CachedLine;
+
+typedef struct {
+ int lineIndex; /* index into LayoutCache.lines */
+ int srcStart, srcEnd;
+ int globalSegIndex; /* identifies this segment's Clay element: CLAY_IDI("Seg", globalSegIndex) */
+ int fontId;
+ int fontSize;
+} CachedSegment;
+
+typedef struct {
+ CachedLine *lines;
+ int lineCount, lineCap;
+ CachedSegment *segs;
+ int segCount, segCap;
+} LayoutCache;
+
+void layout_cache_init(LayoutCache *c);
+void layout_cache_reset(LayoutCache *c); /* clears contents but keeps capacity, for reuse each frame */
+void layout_cache_free(LayoutCache *c);
+
+int layout_cache_add_line(LayoutCache *c, int blockIndex, int lineInBlock, int srcStart, int srcEnd);
+int layout_cache_add_seg(LayoutCache *c, int lineIndex, int srcStart, int srcEnd, int fontId, int fontSize);
+void layout_cache_close_line(LayoutCache *c, int lineIndex); /* call after adding all segs for a line, to set segEnd */
+
+#endif
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..f6c7342
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,183 @@
+#include "raylib.h"
+#include "clay.h"
+#include "editor.h"
+#include "render.h"
+#include "layout_cache.h"
+#include "fonts.h"
+#include "theme.h"
+#include "app_mode.h"
+#include "config.h"
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+static void handle_clay_errors(Clay_ErrorData errorData) {
+ fprintf(stderr, "Clay error: %s\n", errorData.errorText.chars);
+}
+
+/* True on the frame a key was just pressed, and then again at a steady repeat rate while held. */
+static bool key_repeat(int key, double now, double *nextRepeatAt) {
+ if (IsKeyPressed(key)) { *nextRepeatAt = now + 0.4; return true; }
+ if (IsKeyDown(key) && now >= *nextRepeatAt) { *nextRepeatAt = now + 0.03; return true; }
+ if (!IsKeyDown(key)) *nextRepeatAt = 0.0;
+ return false;
+}
+
+static void update_window_title(EditorState *ed) {
+ char title[512];
+ snprintf(title, sizeof title, "%s%s \xe2\x80\x94 hush", ed->dirty ? "*" : "", editor_display_name(ed));
+ SetWindowTitle(title);
+}
+
+int main(int argc, char **argv) {
+ Config cfg;
+ config_load(&cfg); /* pure file I/O, no raylib dependency -- safe to run before window init */
+ theme_init_defaults();
+ g_theme.bg = cfg.bgColor;
+ g_theme.text = cfg.textColor;
+ g_theme.cursor = cfg.textColor; /* caret always tracks the configured text color */
+
+ Clay_Raylib_Initialize(1000, 800, "hush", FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_MSAA_4X_HINT);
+ SetExitKey(KEY_NULL);
+
+ uint64_t memSize = Clay_MinMemorySize();
+ Clay_Arena arena = Clay_CreateArenaWithCapacityAndMemory(memSize, malloc(memSize));
+ Clay_Initialize(arena, (Clay_Dimensions){ (float)GetScreenWidth(), (float)GetScreenHeight() }, (Clay_ErrorHandler){ handle_clay_errors, 0 });
+
+ Font fonts[FONT_COUNT];
+ fonts_load(fonts, cfg.fontPaths);
+ config_free(&cfg);
+ Clay_SetMeasureTextFunction(hush_measure_text, fonts);
+
+ EditorState ed;
+ editor_init(&ed);
+ if (argc >= 2) {
+ editor_load(&ed, argv[1]);
+ }
+ update_window_title(&ed);
+
+ LayoutCache cache;
+ layout_cache_init(&cache);
+
+ double blinkResetTime = GetTime();
+ int lastBlock = ed.cursorBlock, lastOffset = ed.cursorOffset;
+ bool lastDirty = ed.dirty;
+
+ double repLeft = 0, repRight = 0, repUp = 0, repDown = 0, repBackspace = 0, repDelete = 0;
+
+ bool windowClosing = false;
+ AppMode mode = MODE_EDITING;
+ /* True only for a mouse-down session whose *press* frame was itself handled in
+ MODE_EDITING -- prevents the tail end of a click that dismissed a modal (still
+ physically held down for a frame or two afterward) from being replayed as a
+ document drag-select once mode flips back to MODE_EDITING. */
+ bool mouseDownInEditor = false;
+
+ while (!windowClosing) {
+ double now = GetTime();
+ bool ctrl = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
+ bool shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
+
+ if (mode == MODE_EDITING) {
+ if (ctrl && IsKeyPressed(KEY_S)) {
+ editor_save(&ed);
+ } else if (ctrl && IsKeyPressed(KEY_V)) {
+ const char *clip = GetClipboardText();
+ if (clip) editor_paste(&ed, clip, (int)strlen(clip));
+ } else if (ctrl && IsKeyPressed(KEY_C)) {
+ int len;
+ char *text = editor_selection_to_text(&ed, &len);
+ if (text) { SetClipboardText(text); free(text); }
+ } else if (ctrl && shift && IsKeyPressed(KEY_Z)) {
+ editor_redo(&ed);
+ } else if (ctrl && IsKeyPressed(KEY_Z)) {
+ editor_undo(&ed);
+ } else if (ctrl && IsKeyPressed(KEY_X)) {
+ mode = ed.dirty ? MODE_QUIT_CONFIRM : MODE_EDITING;
+ if (!ed.dirty) windowClosing = true;
+ } else if ((ctrl && IsKeyPressed(KEY_SLASH)) || IsKeyPressed(KEY_F1)) {
+ /* Ctrl+/ is bound by physical scancode (matches the common editor convention,
+ e.g. VS Code/Sublime's comment-toggle), which lands on a different printed
+ character on non-US keyboard layouts (e.g. it's the "-/_" key on a Swedish
+ layout, not "?"). F1 is a layout-independent fallback that's always reachable. */
+ mode = MODE_SHORTCUTS_HELP;
+ } else {
+ if (!ctrl) {
+ int cp;
+ while ((cp = GetCharPressed()) != 0) {
+ int utf8Size = 0;
+ const char *bytes = CodepointToUTF8(cp, &utf8Size);
+ editor_insert_utf8(&ed, bytes, utf8Size);
+ }
+ }
+ if (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER)) editor_enter(&ed);
+ if (key_repeat(KEY_BACKSPACE, now, &repBackspace)) editor_backspace(&ed);
+ if (key_repeat(KEY_DELETE, now, &repDelete)) editor_delete_forward(&ed);
+ if (key_repeat(KEY_LEFT, now, &repLeft)) editor_move_left(&ed, shift);
+ if (key_repeat(KEY_RIGHT, now, &repRight)) editor_move_right(&ed, shift);
+ if (key_repeat(KEY_UP, now, &repUp)) editor_move_vertical(&ed, &cache, fonts, -1, shift);
+ if (key_repeat(KEY_DOWN, now, &repDown)) editor_move_vertical(&ed, &cache, fonts, 1, shift);
+ if (IsKeyPressed(KEY_HOME)) editor_move_home(&ed, shift);
+ if (IsKeyPressed(KEY_END)) editor_move_end(&ed, shift);
+ }
+
+ if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+ editor_click(&ed, &cache, fonts, GetMousePosition());
+ mouseDownInEditor = true;
+ } else if (mouseDownInEditor && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
+ editor_drag_to(&ed, &cache, fonts, GetMousePosition());
+ }
+
+ /* raylib's WindowShouldClose() self-resets every frame (PollInputEvents re-derives
+ it from the GLFW window-close flag and immediately clears that flag), so this is
+ a clean one-shot edge per OS close-button click -- no extra latch needed. */
+ if (WindowShouldClose()) {
+ mode = ed.dirty ? MODE_QUIT_CONFIRM : MODE_EDITING;
+ if (!ed.dirty) windowClosing = true;
+ }
+ } else if (mode == MODE_QUIT_CONFIRM) {
+ if (IsKeyPressed(KEY_ESCAPE)) mode = MODE_EDITING;
+ } else if (mode == MODE_SHORTCUTS_HELP) {
+ if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressed(KEY_F1) || (ctrl && IsKeyPressed(KEY_SLASH))) mode = MODE_EDITING;
+ }
+
+ if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) mouseDownInEditor = false;
+
+ if (ed.cursorBlock != lastBlock || ed.cursorOffset != lastOffset) {
+ blinkResetTime = now;
+ lastBlock = ed.cursorBlock;
+ lastOffset = ed.cursorOffset;
+ }
+ if (ed.dirty != lastDirty) {
+ lastDirty = ed.dirty;
+ update_window_title(&ed);
+ }
+
+ Clay_Vector2 mousePos = { GetMousePosition().x, GetMousePosition().y };
+ Clay_SetPointerState(mousePos, IsMouseButtonDown(0));
+ Clay_SetLayoutDimensions((Clay_Dimensions){ (float)GetScreenWidth(), (float)GetScreenHeight() });
+ Vector2 wheel = GetMouseWheelMoveV();
+ Clay_UpdateScrollContainers(false, (Clay_Vector2){ wheel.x, wheel.y * 40.0f }, GetFrameTime());
+
+ float caretBlinkT = (float)(now - blinkResetTime);
+
+ BeginDrawing();
+ ClearBackground(BLACK);
+ ModalAction action = render_frame(&ed, &cache, fonts, caretBlinkT, mode);
+ EndDrawing();
+
+ switch (action) {
+ case MODAL_ACTION_SAVE_QUIT: editor_save(&ed); windowClosing = true; break;
+ case MODAL_ACTION_DISCARD_QUIT: windowClosing = true; break;
+ case MODAL_ACTION_CANCEL: mode = MODE_EDITING; break;
+ case MODAL_ACTION_CLOSE_HELP: mode = MODE_EDITING; break;
+ default: break;
+ }
+ }
+
+ layout_cache_free(&cache);
+ editor_free(&ed);
+ fonts_unload(fonts);
+ Clay_Raylib_Close();
+ return 0;
+}
diff --git a/src/markdown_io.c b/src/markdown_io.c
new file mode 100644
index 0000000..55d7ce4
--- /dev/null
+++ b/src/markdown_io.c
@@ -0,0 +1,167 @@
+#include "markdown_io.h"
+#include "strbuf.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+static int match_numbered_prefix(const char *line, int len, int *prefixLen) {
+ int i = 0;
+ while (i < len && isdigit((unsigned char)line[i])) i++;
+ if (i == 0 || i >= len || line[i] != '.') return 0;
+ i++;
+ if (i < len && line[i] == ' ') i++;
+ else if (i < len) return 0; /* digits+'.' followed by something other than a space: not a list marker */
+ *prefixLen = i;
+ return 1;
+}
+
+static int is_blank_line(const char *line, int len) {
+ for (int i = 0; i < len; i++) {
+ if (line[i] != ' ' && line[i] != '\t') return 0;
+ }
+ return 1;
+}
+
+static int is_code_fence(const char *line, int len) {
+ return len >= 3 && line[0] == '`' && line[1] == '`' && line[2] == '`';
+}
+
+bool document_load_file(Document *doc, const char *path) {
+ FILE *f = fopen(path, "rb");
+ if (!f) return false;
+
+ fseek(f, 0, SEEK_END);
+ long size = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ if (size < 0) { fclose(f); return false; }
+
+ char *buf = malloc((size_t)size + 1);
+ size_t readN = fread(buf, 1, (size_t)size, f);
+ fclose(f);
+ buf[readN] = '\0';
+
+ for (int i = 0; i < doc->count; i++) sb_free(&doc->blocks[i].text);
+ doc->count = 0;
+
+ int pos = 0;
+ int len = (int)readN;
+ while (pos < len) {
+ int lineStart = pos;
+ while (pos < len && buf[pos] != '\n') pos++;
+ int lineEnd = pos;
+ if (lineEnd > lineStart && buf[lineEnd - 1] == '\r') lineEnd--;
+ if (pos < len) pos++; /* consume '\n' */
+
+ const char *line = buf + lineStart;
+ int lineLen = lineEnd - lineStart;
+
+ if (is_code_fence(line, lineLen)) {
+ StrBuf code;
+ sb_init(&code);
+ int first = 1;
+ while (pos < len) {
+ int s = pos;
+ while (pos < len && buf[pos] != '\n') pos++;
+ int e = pos;
+ if (e > s && buf[e - 1] == '\r') e--;
+ if (pos < len) pos++;
+ if (is_code_fence(buf + s, e - s)) break;
+ if (!first) sb_append_char(&code, '\n');
+ sb_append(&code, buf + s, e - s);
+ first = 0;
+ }
+ document_insert_block(doc, doc->count, BLOCK_CODE, code.data, code.len);
+ sb_free(&code);
+ continue;
+ }
+
+ if (is_blank_line(line, lineLen)) continue;
+
+ int prefixLen;
+ if (lineLen >= 2 && line[0] == '#' && line[1] == ' ') {
+ document_insert_block(doc, doc->count, BLOCK_H1, line + 2, lineLen - 2);
+ } else if (lineLen >= 3 && line[0] == '#' && line[1] == '#' && line[2] == ' ') {
+ document_insert_block(doc, doc->count, BLOCK_H2, line + 3, lineLen - 3);
+ } else if (lineLen >= 4 && line[0] == '#' && line[1] == '#' && line[2] == '#' && line[3] == ' ') {
+ document_insert_block(doc, doc->count, BLOCK_H3, line + 4, lineLen - 4);
+ } else if (lineLen >= 2 && (line[0] == '-' || line[0] == '*') && line[1] == ' ') {
+ document_insert_block(doc, doc->count, BLOCK_BULLET, line + 2, lineLen - 2);
+ } else if (lineLen >= 2 && line[0] == '>' && line[1] == ' ') {
+ document_insert_block(doc, doc->count, BLOCK_QUOTE, line + 2, lineLen - 2);
+ } else if (match_numbered_prefix(line, lineLen, &prefixLen)) {
+ document_insert_block(doc, doc->count, BLOCK_NUMBERED, line + prefixLen, lineLen - prefixLen);
+ } else {
+ document_insert_block(doc, doc->count, BLOCK_PARAGRAPH, line, lineLen);
+ }
+ }
+
+ free(buf);
+
+ if (doc->count == 0) {
+ document_insert_block(doc, 0, BLOCK_PARAGRAPH, "", 0);
+ }
+
+ return true;
+}
+
+void document_serialize(Document *doc, StrBuf *out) {
+ sb_clear(out);
+
+ int numberedRun = 0;
+ for (int i = 0; i < doc->count; i++) {
+ Block *b = &doc->blocks[i];
+ if (i > 0) sb_append(out, "\n\n", 2);
+
+ switch (b->type) {
+ case BLOCK_H1: sb_append(out, "# ", 2); sb_append(out, b->text.data, b->text.len); break;
+ case BLOCK_H2: sb_append(out, "## ", 3); sb_append(out, b->text.data, b->text.len); break;
+ case BLOCK_H3: sb_append(out, "### ", 4); sb_append(out, b->text.data, b->text.len); break;
+ case BLOCK_BULLET: sb_append(out, "- ", 2); sb_append(out, b->text.data, b->text.len); break;
+ case BLOCK_NUMBERED: {
+ if (i > 0 && doc->blocks[i - 1].type == BLOCK_NUMBERED) numberedRun++;
+ else numberedRun = 1;
+ char prefix[16];
+ int n = snprintf(prefix, sizeof prefix, "%d. ", numberedRun);
+ sb_append(out, prefix, n);
+ sb_append(out, b->text.data, b->text.len);
+ break;
+ }
+ case BLOCK_QUOTE: sb_append(out, "> ", 2); sb_append(out, b->text.data, b->text.len); break;
+ case BLOCK_CODE:
+ sb_append(out, "```\n", 4);
+ sb_append(out, b->text.data, b->text.len);
+ sb_append(out, "\n```", 4);
+ break;
+ case BLOCK_PARAGRAPH:
+ default:
+ sb_append(out, b->text.data, b->text.len);
+ break;
+ }
+ }
+ sb_append_char(out, '\n');
+}
+
+bool document_save_file(Document *doc, const char *path) {
+ StrBuf out;
+ sb_init(&out);
+ document_serialize(doc, &out);
+
+ FILE *f = fopen(path, "wb");
+ if (!f) { sb_free(&out); return false; }
+ size_t written = fwrite(out.data, 1, (size_t)out.len, f);
+ fclose(f);
+ sb_free(&out);
+ return written == (size_t)out.len || out.len == 0;
+}
+
+void document_compute_stats(Document *doc, DocStats *out) {
+ StrBuf tmp;
+ sb_init(&tmp);
+ document_serialize(doc, &tmp);
+ out->byteSize = tmp.len;
+ int lines = 0;
+ for (int i = 0; i < tmp.len; i++) if (tmp.data[i] == '\n') lines++;
+ out->lineCount = lines;
+ sb_free(&tmp);
+}
diff --git a/src/markdown_io.h b/src/markdown_io.h
new file mode 100644
index 0000000..f5dc4e9
--- /dev/null
+++ b/src/markdown_io.h
@@ -0,0 +1,26 @@
+#ifndef HUSH_MARKDOWN_IO_H
+#define HUSH_MARKDOWN_IO_H
+
+#include "document.h"
+#include <stdbool.h>
+
+/* Replaces the contents of `doc` with the parsed contents of the file at `path`.
+ Returns false (and leaves `doc` untouched) if the file could not be read. */
+bool document_load_file(Document *doc, const char *path);
+
+/* Serializes `doc` to markdown and writes it to `path`. Returns false on I/O failure. */
+bool document_save_file(Document *doc, const char *path);
+
+/* Serializes `doc` to markdown into `out` (cleared first). The single source of truth for
+ markdown serialization -- both document_save_file and document_compute_stats use it. */
+void document_serialize(Document *doc, StrBuf *out);
+
+typedef struct {
+ long byteSize; /* length of the serialized markdown, in bytes */
+ int lineCount; /* number of '\n' bytes in the serialized markdown (wc -l semantics) */
+} DocStats;
+
+/* Computes size/line-count stats for `doc` via document_serialize, without touching disk. */
+void document_compute_stats(Document *doc, DocStats *out);
+
+#endif
diff --git a/src/render.c b/src/render.c
new file mode 100644
index 0000000..675a516
--- /dev/null
+++ b/src/render.c
@@ -0,0 +1,507 @@
+#include "render.h"
+#include "clay.h"
+#include "theme.h"
+#include "fonts.h"
+#include "inline_parse.h"
+#include "wrap.h"
+#include "hittest.h"
+#include "markdown_io.h"
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+typedef enum { RK_PLAIN, RK_BOLD, RK_ITALIC, RK_BOLD_ITALIC, RK_CODE, RK_LINK, RK_MARKER } RenderKind;
+
+typedef struct {
+ RenderKind kind;
+ int start, end; /* byte range in the block's raw text to actually display */
+} RenderRun;
+
+typedef struct {
+ Font *fonts;
+ LayoutCache *cache;
+ float contentWidth;
+ int cursorBlock;
+ int cursorOffset;
+} RenderCtx;
+
+typedef struct {
+ Font *fonts;
+ RenderRun *runs;
+ bool boldBase;
+ int baseFontSize;
+} MeasureCtx;
+
+/* Small per-frame bump allocator for text that Clay needs to keep pointing at after this
+ function returns (e.g. a formatted "3." list marker) — Clay stores the Clay_String's
+ pointer, not a copy, so it must stay valid until this frame's render commands are drawn. */
+#define SCRATCH_SIZE (64 * 1024)
+static char g_scratch[SCRATCH_SIZE];
+static int g_scratchPos;
+
+static char *scratch_alloc(int n) {
+ if (g_scratchPos + n > SCRATCH_SIZE) n = SCRATCH_SIZE - g_scratchPos;
+ if (n < 0) n = 0;
+ char *p = g_scratch + g_scratchPos;
+ g_scratchPos += n;
+ return p;
+}
+
+static void resolve_style(RenderKind kind, bool boldBase, int baseFontSize, Clay_Color baseColor,
+ int *fontId, int *fontSize, Clay_Color *color) {
+ bool mono = (kind == RK_CODE);
+ bool bold = boldBase || kind == RK_BOLD || kind == RK_BOLD_ITALIC;
+ bool italic = kind == RK_ITALIC || kind == RK_BOLD_ITALIC;
+ if (kind == RK_MARKER) { mono = false; bold = false; italic = false; }
+ *fontId = font_index(mono, bold, italic);
+ *fontSize = baseFontSize;
+ switch (kind) {
+ case RK_MARKER: *color = COL_MARKER; break;
+ case RK_LINK: *color = COL_LINK; break;
+ case RK_CODE: *color = COL_CODE_TEXT; break;
+ default: *color = baseColor; break;
+ }
+}
+
+static float measure_cb(void *ctxV, int runIndex, const char *text, int start, int len) {
+ if (len <= 0) return 0.0f;
+ MeasureCtx *ctx = (MeasureCtx *)ctxV;
+ RenderKind kind = ctx->runs[runIndex].kind;
+ int fontId, fontSize; Clay_Color color;
+ resolve_style(kind, ctx->boldBase, ctx->baseFontSize, g_theme.text, &fontId, &fontSize, &color);
+
+ char buf[1024];
+ int n = len;
+ if (n >= (int)sizeof(buf)) n = sizeof(buf) - 1;
+ memcpy(buf, text + start, (size_t)n);
+ buf[n] = '\0';
+ Vector2 sz = MeasureTextEx(ctx->fonts[fontId], buf, (float)fontSize, 0.0f);
+ return sz.x;
+}
+
+/* Expands parsed inline runs into RenderRuns: styled markup is shown as plain rendered text
+ with markers hidden, unless the cursor sits inside that run, in which case the raw markers
+ are revealed (dimmed) around the still-styled content — WYSIWYG with an editable escape hatch. */
+static void expand_runs(InlineRunList *runs, int cursorBlock, int cursorOffset, int thisBlockIndex,
+ RenderRun **outArr, int *outCount) {
+ int cap = runs->count * 3 + 1;
+ RenderRun *arr = malloc((size_t)cap * sizeof(RenderRun));
+ int n = 0;
+ bool hasCursor = (thisBlockIndex == cursorBlock);
+
+ for (int i = 0; i < runs->count; i++) {
+ InlineRun *r = &runs->runs[i];
+ if (r->kind == RUN_PLAIN) {
+ arr[n++] = (RenderRun){ RK_PLAIN, r->contentStart, r->contentEnd };
+ continue;
+ }
+ if (r->kind == RUN_CODE) {
+ /* Inline code markers are always hidden; the chip background carries the meaning. */
+ arr[n++] = (RenderRun){ RK_CODE, r->contentStart, r->contentEnd };
+ continue;
+ }
+
+ RenderKind styleKind = r->kind == RUN_BOLD ? RK_BOLD
+ : r->kind == RUN_ITALIC ? RK_ITALIC
+ : r->kind == RUN_BOLD_ITALIC ? RK_BOLD_ITALIC
+ : RK_LINK;
+ bool focused = hasCursor && cursorOffset >= r->rawStart && cursorOffset <= r->rawEnd;
+ if (!focused) {
+ arr[n++] = (RenderRun){ styleKind, r->contentStart, r->contentEnd };
+ } else {
+ if (r->contentStart > r->rawStart) arr[n++] = (RenderRun){ RK_MARKER, r->rawStart, r->contentStart };
+ arr[n++] = (RenderRun){ styleKind, r->contentStart, r->contentEnd };
+ if (r->rawEnd > r->contentEnd) arr[n++] = (RenderRun){ RK_MARKER, r->contentEnd, r->rawEnd };
+ }
+ }
+
+ *outArr = arr;
+ *outCount = n;
+}
+
+static int compute_list_number(Document *doc, int blockIndex) {
+ int n = 1;
+ int i = blockIndex - 1;
+ while (i >= 0 && doc->blocks[i].type == BLOCK_NUMBERED) { n++; i--; }
+ return n;
+}
+
+static void emit_wrapped_lines(RenderCtx *ctx, int blockIndex, const char *text, RenderRun *runs, int runCount,
+ float availWidth, int baseFontSize, bool boldBase, Clay_Color baseColor) {
+ WrapRunSpan *spans = malloc((size_t)(runCount > 0 ? runCount : 1) * sizeof(WrapRunSpan));
+ for (int i = 0; i < runCount; i++) { spans[i].start = runs[i].start; spans[i].end = runs[i].end; }
+
+ MeasureCtx mctx = { ctx->fonts, runs, boldBase, baseFontSize };
+ WrapResult wr;
+ wrap_layout(text, spans, runCount, availWidth, measure_cb, &mctx, &wr);
+ free(spans);
+
+ float lineHeight = (float)baseFontSize * LINE_HEIGHT_MULT;
+
+ for (int li = 0; li < wr.count; li++) {
+ WrapLine *wl = &wr.lines[li];
+ int srcStart = 0, srcEnd = 0;
+ if (wl->count > 0) { srcStart = wl->segs[0].start; srcEnd = wl->segs[wl->count - 1].end; }
+
+ int lineIdx = layout_cache_add_line(ctx->cache, blockIndex, li, srcStart, srcEnd);
+
+ CLAY(CLAY_IDI("Line", lineIdx), {
+ .layout = {
+ .layoutDirection = CLAY_LEFT_TO_RIGHT,
+ .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(lineHeight) },
+ .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
+ },
+ }) {
+ for (int si = 0; si < wl->count; si++) {
+ WrapSegment *ws = &wl->segs[si];
+ RenderKind kind = runs[ws->runIndex].kind;
+ int fontId, fontSize; Clay_Color color;
+ resolve_style(kind, boldBase, baseFontSize, baseColor, &fontId, &fontSize, &color);
+
+ int segIdx = layout_cache_add_seg(ctx->cache, lineIdx, ws->start, ws->end, fontId, fontSize);
+ Clay_String txt = { .isStaticallyAllocated = false, .length = ws->end - ws->start, .chars = text + ws->start };
+ Clay_TextElementConfig tc = { .fontId = (uint16_t)fontId, .fontSize = (uint16_t)fontSize, .textColor = color, .wrapMode = CLAY_TEXT_WRAP_NONE };
+
+ if (kind == RK_CODE) {
+ CLAY(CLAY_IDI("Seg", segIdx), {
+ .layout = { .padding = { 5, 5, 1, 1 } },
+ .backgroundColor = COL_CODE_BG,
+ .cornerRadius = CLAY_CORNER_RADIUS(4),
+ }) {
+ CLAY_TEXT(txt, tc);
+ }
+ } else {
+ CLAY(CLAY_IDI("Seg", segIdx), {
+ .layout = { .sizing = { CLAY_SIZING_FIT(0), CLAY_SIZING_FIT(0) } },
+ }) {
+ CLAY_TEXT(txt, tc);
+ }
+ }
+ }
+ }
+ layout_cache_close_line(ctx->cache, lineIdx);
+ }
+
+ wrap_result_free(&wr);
+}
+
+static void emit_code_block(RenderCtx *ctx, int blockIndex, const char *text, int len) {
+ int fontId = FONT_MONO_REGULAR;
+ int fontSize = FS_CODE;
+ float lineHeight = (float)fontSize * LINE_HEIGHT_MULT;
+ int lineInBlock = 0;
+ int pos = 0;
+
+ for (;;) {
+ int start = pos;
+ while (pos < len && text[pos] != '\n') pos++;
+ int end = pos;
+
+ int lineIdx = layout_cache_add_line(ctx->cache, blockIndex, lineInBlock, start, end);
+ CLAY(CLAY_IDI("Line", lineIdx), {
+ .layout = {
+ .layoutDirection = CLAY_LEFT_TO_RIGHT,
+ .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(lineHeight) },
+ .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
+ },
+ }) {
+ if (end > start) {
+ int segIdx = layout_cache_add_seg(ctx->cache, lineIdx, start, end, fontId, fontSize);
+ Clay_String txt = { .isStaticallyAllocated = false, .length = end - start, .chars = text + start };
+ CLAY(CLAY_IDI("Seg", segIdx), {
+ .layout = { .sizing = { CLAY_SIZING_FIT(0), CLAY_SIZING_FIT(0) } },
+ }) {
+ CLAY_TEXT(txt, CLAY_TEXT_CONFIG({ .fontId = (uint16_t)fontId, .fontSize = (uint16_t)fontSize, .textColor = COL_CODEBLOCK_TEXT, .wrapMode = CLAY_TEXT_WRAP_NONE }));
+ }
+ }
+ }
+ layout_cache_close_line(ctx->cache, lineIdx);
+
+ if (pos >= len) break;
+ pos++;
+ lineInBlock++;
+ }
+}
+
+static void emit_block(RenderCtx *ctx, Document *doc, int blockIndex) {
+ Block *b = &doc->blocks[blockIndex];
+
+ int baseFontSize = FS_BODY;
+ bool boldBase = false;
+ Clay_Color baseColor = g_theme.text;
+ int topPad = 3;
+
+ switch (b->type) {
+ case BLOCK_H1: baseFontSize = FS_H1; boldBase = true; topPad = 22; break;
+ case BLOCK_H2: baseFontSize = FS_H2; boldBase = true; topPad = 16; break;
+ case BLOCK_H3: baseFontSize = FS_H3; boldBase = true; topPad = 10; break;
+ case BLOCK_QUOTE: baseColor = COL_TEXT_QUOTE; break;
+ default: break;
+ }
+
+ Clay_ElementDeclaration decl = {0};
+ decl.layout.layoutDirection = CLAY_TOP_TO_BOTTOM;
+ decl.layout.sizing.width = CLAY_SIZING_FIXED(ctx->contentWidth);
+ decl.layout.sizing.height = CLAY_SIZING_FIT(0);
+ decl.layout.padding.top = (uint16_t)topPad;
+ decl.layout.padding.bottom = BLOCK_GAP;
+
+ if (b->type == BLOCK_QUOTE) {
+ decl.border.color = COL_QUOTE_BORDER;
+ decl.border.width.left = 4;
+ decl.layout.padding.left = QUOTE_INDENT;
+ }
+ if (b->type == BLOCK_CODE) {
+ decl.backgroundColor = COL_CODEBLOCK_BG;
+ decl.cornerRadius = CLAY_CORNER_RADIUS(6);
+ decl.layout.padding.left = 12;
+ decl.layout.padding.right = 12;
+ decl.layout.padding.top = (uint16_t)(topPad + 8);
+ decl.layout.padding.bottom = (uint16_t)(BLOCK_GAP + 8);
+ }
+
+ CLAY(CLAY_IDI("Block", blockIndex), decl) {
+ if (b->type == BLOCK_CODE) {
+ emit_code_block(ctx, blockIndex, b->text.data, b->text.len);
+ } else if (b->type == BLOCK_BULLET || b->type == BLOCK_NUMBERED) {
+ InlineRunList runs;
+ inline_parse(b->text.data, b->text.len, &runs);
+ RenderRun *rr; int rc;
+ expand_runs(&runs, ctx->cursorBlock, ctx->cursorOffset, blockIndex, &rr, &rc);
+ inline_runs_free(&runs);
+
+ CLAY_AUTO_ID({
+ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT, .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIT(0) } },
+ }) {
+ CLAY_AUTO_ID({
+ .layout = { .sizing = { CLAY_SIZING_FIXED(LIST_INDENT), CLAY_SIZING_FIT(0) } },
+ }) {
+ Clay_String markerText;
+ if (b->type == BLOCK_BULLET) {
+ markerText = CLAY_STRING("\xE2\x80\xA2");
+ } else {
+ char *buf = scratch_alloc(16);
+ int num = compute_list_number(doc, blockIndex);
+ int n = snprintf(buf, 16, "%d.", num);
+ markerText = (Clay_String){ .isStaticallyAllocated = false, .length = n, .chars = buf };
+ }
+ CLAY_TEXT(markerText, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = (uint16_t)baseFontSize, .textColor = g_theme.text }));
+ }
+ CLAY_AUTO_ID({
+ .layout = { .layoutDirection = CLAY_TOP_TO_BOTTOM, .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIT(0) } },
+ }) {
+ emit_wrapped_lines(ctx, blockIndex, b->text.data, rr, rc, ctx->contentWidth - LIST_INDENT, baseFontSize, boldBase, baseColor);
+ }
+ }
+ free(rr);
+ } else {
+ InlineRunList runs;
+ inline_parse(b->text.data, b->text.len, &runs);
+ RenderRun *rr; int rc;
+ expand_runs(&runs, ctx->cursorBlock, ctx->cursorOffset, blockIndex, &rr, &rc);
+ inline_runs_free(&runs);
+
+ float avail = ctx->contentWidth - (b->type == BLOCK_QUOTE ? QUOTE_INDENT : 0);
+ emit_wrapped_lines(ctx, blockIndex, b->text.data, rr, rc, avail, baseFontSize, boldBase, baseColor);
+ free(rr);
+ }
+ }
+}
+
+static void modal_text_line(const char *text) {
+ Clay_String s = { .isStaticallyAllocated = true, .length = (int)strlen(text), .chars = text };
+ CLAY_TEXT(s, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = FS_MODAL, .textColor = g_theme.text, .wrapMode = CLAY_TEXT_WRAP_NONE }));
+}
+
+/* A clickable modal button: highlights on hover, and sets *action to `thisAction` on click. */
+static void modal_button(Clay_ElementId id, const char *label, ModalAction *action, ModalAction thisAction) {
+ bool hovered = Clay_PointerOver(id);
+ CLAY(id, {
+ .layout = {
+ .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(36) },
+ .padding = { 12, 12, 0, 0 },
+ .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
+ },
+ .backgroundColor = hovered ? COL_BUTTON_HOVER : g_theme.bg,
+ .cornerRadius = CLAY_CORNER_RADIUS(4),
+ .border = { .color = COL_MODAL_BORDER, .width = CLAY_BORDER_OUTSIDE(1) },
+ }) {
+ Clay_String s = { .isStaticallyAllocated = true, .length = (int)strlen(label), .chars = label };
+ CLAY_TEXT(s, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = FS_MODAL, .textColor = g_theme.text }));
+ }
+ if (hovered && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) *action = thisAction;
+}
+
+/* Draws the quit-confirm or shortcuts-help modal (whichever `mode` calls for) as a floating,
+ dimmed overlay on top of everything else, and returns the action the user's click resolved
+ to this frame (MODAL_ACTION_NONE if none). No-op when mode == MODE_EDITING. */
+static ModalAction emit_modal(AppMode mode, float winW, float winH) {
+ ModalAction action = MODAL_ACTION_NONE;
+ if (mode == MODE_EDITING) return action;
+
+ CLAY(CLAY_ID("ModalScrim"), {
+ .layout = {
+ .sizing = { CLAY_SIZING_FIXED(winW), CLAY_SIZING_FIXED(winH) },
+ .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
+ },
+ .backgroundColor = COL_MODAL_SCRIM,
+ .floating = { .attachTo = CLAY_ATTACH_TO_ROOT, .zIndex = 10 },
+ }) {
+ if (Clay_PointerOver(CLAY_ID("ModalScrim")) && !Clay_PointerOver(CLAY_ID("ModalBox")) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+ action = (mode == MODE_QUIT_CONFIRM) ? MODAL_ACTION_CANCEL : MODAL_ACTION_CLOSE_HELP;
+ }
+
+ CLAY(CLAY_ID("ModalBox"), {
+ .layout = {
+ .layoutDirection = CLAY_TOP_TO_BOTTOM,
+ .sizing = { CLAY_SIZING_FIXED(mode == MODE_QUIT_CONFIRM ? 380 : 420), CLAY_SIZING_FIT(0) },
+ .padding = CLAY_PADDING_ALL(24),
+ .childGap = 12,
+ },
+ .backgroundColor = g_theme.bg,
+ .cornerRadius = CLAY_CORNER_RADIUS(8),
+ .border = { .color = COL_MODAL_BORDER, .width = CLAY_BORDER_OUTSIDE(1) },
+ }) {
+ if (mode == MODE_QUIT_CONFIRM) {
+ CLAY_TEXT(CLAY_STRING("Spara \xC3\xA4ndringar innan du avslutar?"),
+ CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_BOLD, .fontSize = FS_MODAL, .textColor = g_theme.text }));
+ modal_button(CLAY_ID("BtnSaveQuit"), "Spara och avsluta", &action, MODAL_ACTION_SAVE_QUIT);
+ modal_button(CLAY_ID("BtnDiscardQuit"), "Avsluta utan att spara", &action, MODAL_ACTION_DISCARD_QUIT);
+ modal_button(CLAY_ID("BtnCancel"), "Avbryt", &action, MODAL_ACTION_CANCEL);
+ } else {
+ CLAY_TEXT(CLAY_STRING("Kortkommandon"),
+ CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_BOLD, .fontSize = FS_MODAL, .textColor = g_theme.text }));
+ modal_text_line("Piltangenter \xE2\x80\x94 flytta mark\xC3\xB6ren");
+ modal_text_line("Shift + piltangenter \xE2\x80\x94 markera text");
+ modal_text_line("Musklick + drag \xE2\x80\x94 markera text");
+ modal_text_line("Ctrl+S \xE2\x80\x94 Spara");
+ modal_text_line("Ctrl+C / Ctrl+V \xE2\x80\x94 Kopiera / klistra in");
+ modal_text_line("Ctrl+Z / Ctrl+Shift+Z \xE2\x80\x94 \xC3\x85ngra / g\xC3\xB6r om");
+ modal_text_line("Ctrl+X \xE2\x80\x94 Avsluta");
+ modal_text_line("F1 \xE2\x80\x94 Den h\xC3\xA4r hj\xC3\xA4lpen");
+ modal_text_line("Enter \xE2\x80\x94 Ny rad / nytt block");
+ modal_text_line("Backspace / Delete \xE2\x80\x94 Ta bort tecken");
+ modal_text_line("Home / End \xE2\x80\x94 Radens b\xC3\xB6rjan/slut");
+ modal_button(CLAY_ID("BtnCloseHelp"), "St\xC3\xA4ng", &action, MODAL_ACTION_CLOSE_HELP);
+ }
+ }
+ }
+ return action;
+}
+
+Clay_Dimensions hush_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
+ Font *fonts = (Font *)userData;
+ Font font = fonts[config->fontId];
+ if (font.glyphs == NULL) font = GetFontDefault();
+
+ char buf[1024];
+ int n = text.length;
+ if (n >= (int)sizeof(buf)) n = sizeof(buf) - 1;
+ if (n > 0) memcpy(buf, text.chars, (size_t)n);
+ buf[n] = '\0';
+
+ Vector2 sz = MeasureTextEx(font, buf, (float)config->fontSize, 0.0f);
+ return (Clay_Dimensions){ sz.x, sz.y > 0 ? sz.y : (float)config->fontSize };
+}
+
+ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float caretBlinkT, AppMode mode) {
+ g_scratchPos = 0;
+ layout_cache_reset(cache);
+
+ float winW = (float)GetScreenWidth();
+ float winH = (float)GetScreenHeight();
+ float contentWidth = winW - 2.0f * CONTENT_SIDE_PADDING;
+ if (contentWidth > CONTENT_MAX_WIDTH) contentWidth = CONTENT_MAX_WIDTH;
+ if (contentWidth < 100.0f) contentWidth = 100.0f;
+
+ RenderCtx ctx = { fonts, cache, contentWidth, ed->cursorBlock, ed->cursorOffset };
+
+ Clay_BeginLayout();
+
+ CLAY(CLAY_ID("Root"), {
+ .layout = { .sizing = { CLAY_SIZING_FIXED(winW), CLAY_SIZING_FIXED(winH) }, .layoutDirection = CLAY_TOP_TO_BOTTOM },
+ .backgroundColor = g_theme.bg,
+ }) {
+ CLAY(CLAY_ID("ScrollArea"), {
+ .layout = {
+ .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0) },
+ .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
+ .padding = { 0, 0, 28, 48 },
+ },
+ .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() },
+ }) {
+ CLAY(CLAY_ID("ContentColumn"), {
+ .layout = { .layoutDirection = CLAY_TOP_TO_BOTTOM, .sizing = { CLAY_SIZING_FIXED(contentWidth), CLAY_SIZING_FIT(0) } },
+ }) {
+ for (int i = 0; i < ed->doc.count; i++) {
+ emit_block(&ctx, &ed->doc, i);
+ }
+ }
+ }
+
+ CLAY(CLAY_ID("StatusBar"), {
+ .layout = {
+ .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(STATUSBAR_HEIGHT) },
+ .layoutDirection = CLAY_LEFT_TO_RIGHT,
+ .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
+ .padding = { STATUSBAR_PADDING, STATUSBAR_PADDING, 0, 0 },
+ },
+ .backgroundColor = COL_STATUSBAR_BG,
+ }) {
+ DocStats stats;
+ document_compute_stats(&ed->doc, &stats);
+ const char *name = editor_display_name(ed);
+
+ char tmp[512];
+ int n = snprintf(tmp, sizeof tmp, "%s%s \xC2\xB7 %.1f KB \xC2\xB7 %d line%s",
+ ed->dirty ? "*" : "", name, stats.byteSize / 1024.0,
+ stats.lineCount, stats.lineCount == 1 ? "" : "s");
+ if (n > (int)sizeof tmp) n = (int)sizeof tmp;
+ char *scratchBuf = scratch_alloc(n);
+ memcpy(scratchBuf, tmp, (size_t)n);
+ Clay_String statusText = { .isStaticallyAllocated = false, .length = n, .chars = scratchBuf };
+ CLAY_TEXT(statusText, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = FS_STATUSBAR,
+ .textColor = COL_STATUSBAR_TEXT, .wrapMode = CLAY_TEXT_WRAP_NONE }));
+ }
+ }
+
+ ModalAction modalAction = emit_modal(mode, winW, winH);
+
+ Clay_RenderCommandArray cmds = Clay_EndLayout(GetFrameTime());
+ Clay_Raylib_Render(cmds, fonts);
+
+ if (mode == MODE_EDITING && editor_has_selection(ed)) {
+ int sb, so, eb, eo;
+ editor_selection_range(ed, &sb, &so, &eb, &eo);
+ Color selColor = {
+ (unsigned char)roundf(COL_SELECTION.r), (unsigned char)roundf(COL_SELECTION.g),
+ (unsigned char)roundf(COL_SELECTION.b), (unsigned char)roundf(COL_SELECTION.a),
+ };
+ for (int i = 0; i < cache->lineCount; i++) {
+ CachedLine *line = &cache->lines[i];
+ if (line->blockIndex < sb || line->blockIndex > eb) continue;
+ int rangeStart = (line->blockIndex == sb) ? so : 0;
+ int rangeEnd = (line->blockIndex == eb) ? eo : ed->doc.blocks[line->blockIndex].text.len;
+ float x, y, w, h;
+ if (hittest_line_range_box(cache, &ed->doc, fonts, i, rangeStart, rangeEnd, &x, &y, &w, &h)) {
+ DrawRectangle((int)x, (int)y, (int)w, (int)h, selColor);
+ }
+ }
+ }
+
+ if (mode == MODE_EDITING) {
+ float cx, cy, ch;
+ if (hittest_caret(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, &cx, &cy, &ch)) {
+ if (fmodf(caretBlinkT, 1.0f) < 0.5f) {
+ Color cursorColor = {
+ (unsigned char)roundf(g_theme.cursor.r), (unsigned char)roundf(g_theme.cursor.g),
+ (unsigned char)roundf(g_theme.cursor.b), (unsigned char)roundf(g_theme.cursor.a),
+ };
+ DrawRectangle((int)cx, (int)cy, 2, (int)ch, cursorColor);
+ }
+ }
+ }
+
+ return modalAction;
+}
diff --git a/src/render.h b/src/render.h
new file mode 100644
index 0000000..ef2a958
--- /dev/null
+++ b/src/render.h
@@ -0,0 +1,29 @@
+#ifndef HUSH_RENDER_H
+#define HUSH_RENDER_H
+
+#include "editor.h"
+#include "layout_cache.h"
+#include "app_mode.h"
+#include "raylib.h"
+#include "clay.h"
+
+/* Declared (not defined) here because vendor/clay_renderer_raylib.c has no header of its own;
+ it is compiled once, into src/clay_impl.c. */
+void Clay_Raylib_Initialize(int width, int height, const char *title, unsigned int flags);
+void Clay_Raylib_Render(Clay_RenderCommandArray renderCommands, Font *fonts);
+void Clay_Raylib_Close(void);
+
+/* Registered with Clay_SetMeasureTextFunction (userData = the Font[FONT_COUNT] array).
+ Uses raylib's own UTF-8 aware MeasureTextEx, unlike the byte-indexed helper that ships
+ with Clay's raylib renderer, so accented characters (e.g. Swedish å ä ö) measure correctly. */
+Clay_Dimensions hush_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
+
+/* Builds the Clay tree for the current editor state, populates `cache` fresh (consumed by
+ hit-testing on the *next* frame's input), issues the Clay render commands, and draws the
+ blinking caret on top. Call between BeginDrawing()/EndDrawing(), after
+ Clay_SetPointerState/Clay_SetLayoutDimensions/Clay_UpdateScrollContainers.
+ When `mode` is not MODE_EDITING, also draws the corresponding modal on top and returns
+ whichever action the user's click resolved to this frame (MODAL_ACTION_NONE otherwise). */
+ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float caretBlinkT, AppMode mode);
+
+#endif
diff --git a/src/strbuf.c b/src/strbuf.c
new file mode 100644
index 0000000..4240556
--- /dev/null
+++ b/src/strbuf.c
@@ -0,0 +1,59 @@
+#include "strbuf.h"
+#include <stdlib.h>
+#include <string.h>
+
+void sb_init(StrBuf *sb) {
+ sb->data = NULL;
+ sb->len = 0;
+ sb->cap = 0;
+}
+
+void sb_free(StrBuf *sb) {
+ free(sb->data);
+ sb->data = NULL;
+ sb->len = 0;
+ sb->cap = 0;
+}
+
+void sb_reserve(StrBuf *sb, int extra) {
+ int needed = sb->len + extra + 1; /* keep a spare byte for a null terminator */
+ if (needed <= sb->cap) return;
+ int newCap = sb->cap > 0 ? sb->cap : 16;
+ while (newCap < needed) newCap *= 2;
+ sb->data = realloc(sb->data, (size_t)newCap);
+ sb->cap = newCap;
+}
+
+void sb_set(StrBuf *sb, const char *s, int n) {
+ sb->len = 0;
+ sb_insert(sb, 0, s, n);
+}
+
+void sb_insert(StrBuf *sb, int pos, const char *s, int n) {
+ if (n <= 0) return;
+ sb_reserve(sb, n);
+ memmove(sb->data + pos + n, sb->data + pos, (size_t)(sb->len - pos));
+ memcpy(sb->data + pos, s, (size_t)n);
+ sb->len += n;
+ sb->data[sb->len] = '\0';
+}
+
+void sb_delete(StrBuf *sb, int pos, int n) {
+ if (n <= 0) return;
+ memmove(sb->data + pos, sb->data + pos + n, (size_t)(sb->len - pos - n));
+ sb->len -= n;
+ if (sb->data) sb->data[sb->len] = '\0';
+}
+
+void sb_append(StrBuf *sb, const char *s, int n) {
+ sb_insert(sb, sb->len, s, n);
+}
+
+void sb_append_char(StrBuf *sb, char c) {
+ sb_append(sb, &c, 1);
+}
+
+void sb_clear(StrBuf *sb) {
+ sb->len = 0;
+ if (sb->data) sb->data[0] = '\0';
+}
diff --git a/src/strbuf.h b/src/strbuf.h
new file mode 100644
index 0000000..605ddbd
--- /dev/null
+++ b/src/strbuf.h
@@ -0,0 +1,20 @@
+#ifndef HUSH_STRBUF_H
+#define HUSH_STRBUF_H
+
+typedef struct {
+ char *data;
+ int len;
+ int cap;
+} StrBuf;
+
+void sb_init(StrBuf *sb);
+void sb_free(StrBuf *sb);
+void sb_reserve(StrBuf *sb, int extra);
+void sb_set(StrBuf *sb, const char *s, int n);
+void sb_insert(StrBuf *sb, int pos, const char *s, int n);
+void sb_delete(StrBuf *sb, int pos, int n);
+void sb_append(StrBuf *sb, const char *s, int n);
+void sb_append_char(StrBuf *sb, char c);
+void sb_clear(StrBuf *sb);
+
+#endif
diff --git a/src/test_main.c b/src/test_main.c
new file mode 100644
index 0000000..9e42c58
--- /dev/null
+++ b/src/test_main.c
@@ -0,0 +1,439 @@
+/* Standalone logic test for the document/editor/markdown-io layer, with no window or GPU
+ context required. Exercises editor.c the same way keystrokes would, and checks the
+ resulting Document state and markdown round-trip directly. */
+#include "editor.h"
+#include "markdown_io.h"
+#include "inline_parse.h"
+#include "undo.h"
+#include "config.h"
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+static int g_failures = 0;
+
+#define CHECK(cond, msg) do { \
+ if (!(cond)) { g_failures++; printf("FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); } \
+ else { printf("ok: %s\n", msg); } \
+} while (0)
+
+static int text_eq(Block *b, const char *s) {
+ int n = (int)strlen(s);
+ return b->text.len == n && memcmp(b->text.data, s, (size_t)n) == 0;
+}
+
+static void type_str(EditorState *ed, const char *s) {
+ for (const char *p = s; *p; p++) editor_insert_utf8(ed, p, 1);
+}
+
+static void test_heading_autoformat(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "# ");
+ CHECK(ed.doc.blocks[0].type == BLOCK_H1, "typing '# ' converts block to H1");
+ CHECK(text_eq(&ed.doc.blocks[0], ""), "H1 marker text is stripped");
+ type_str(&ed, "Titel");
+ CHECK(text_eq(&ed.doc.blocks[0], "Titel"), "H1 block holds typed text");
+ editor_free(&ed);
+}
+
+static void test_bullet_list_continuation(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "- Item1");
+ CHECK(ed.doc.blocks[0].type == BLOCK_BULLET, "typing '- ' converts block to BULLET");
+ CHECK(text_eq(&ed.doc.blocks[0], "Item1"), "bullet block holds typed text");
+
+ editor_enter(&ed);
+ CHECK(ed.doc.count == 2, "Enter in a bullet item adds a new block");
+ CHECK(ed.doc.blocks[1].type == BLOCK_BULLET, "Enter continues the bullet list");
+ CHECK(ed.cursorBlock == 1 && ed.cursorOffset == 0, "cursor moves to the new empty bullet item");
+
+ /* Enter again on the still-empty bullet item exits the list. */
+ editor_enter(&ed);
+ CHECK(ed.doc.count == 2, "Enter on an empty list item does not create a new block");
+ CHECK(ed.doc.blocks[1].type == BLOCK_PARAGRAPH, "Enter on an empty list item exits back to a paragraph");
+ editor_free(&ed);
+}
+
+static void test_numbered_list_renumbers_on_save(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "1. First");
+ editor_enter(&ed);
+ type_str(&ed, "Second");
+ editor_enter(&ed);
+ type_str(&ed, "Third");
+ CHECK(ed.doc.count == 3 && ed.doc.blocks[2].type == BLOCK_NUMBERED, "three numbered items exist");
+
+ document_save_file(&ed.doc, "test_numbered.md");
+ FILE *f = fopen("test_numbered.md", "rb");
+ char buf[256] = {0};
+ size_t n = fread(buf, 1, sizeof buf - 1, f);
+ fclose(f);
+ (void)n;
+ CHECK(strstr(buf, "1. First") && strstr(buf, "2. Second") && strstr(buf, "3. Third"),
+ "saved file renumbers the list sequentially");
+ editor_free(&ed);
+}
+
+static void test_backspace_demotes_then_merges(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "## Heading");
+ editor_move_home(&ed, false);
+ CHECK(ed.doc.blocks[0].type == BLOCK_H2, "starts as H2");
+
+ editor_backspace(&ed);
+ CHECK(ed.doc.blocks[0].type == BLOCK_PARAGRAPH, "backspace at block start demotes H2 to paragraph first");
+ CHECK(text_eq(&ed.doc.blocks[0], "Heading"), "demotion keeps the text intact");
+
+ editor_move_end(&ed, false);
+ editor_enter(&ed);
+ type_str(&ed, "Second");
+ CHECK(ed.doc.count == 2, "now there are two paragraphs");
+
+ ed.cursorBlock = 1;
+ ed.cursorOffset = 0;
+ ed.selAnchorBlock = 1;
+ ed.selAnchorOffset = 0;
+ editor_backspace(&ed);
+ CHECK(ed.doc.count == 1, "backspace at start of a paragraph merges into the previous block");
+ CHECK(text_eq(&ed.doc.blocks[0], "HeadingSecond"), "merge concatenates the text");
+ editor_free(&ed);
+}
+
+static void test_code_fence(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "```");
+ editor_enter(&ed);
+ CHECK(ed.doc.blocks[0].type == BLOCK_CODE, "typing ``` then Enter opens a code block");
+ CHECK(text_eq(&ed.doc.blocks[0], ""), "code block starts empty");
+
+ type_str(&ed, "int main(void) {");
+ editor_enter(&ed);
+ type_str(&ed, " return 0;");
+ editor_enter(&ed);
+ type_str(&ed, "}");
+ editor_enter(&ed);
+ type_str(&ed, "```");
+ editor_enter(&ed);
+
+ CHECK(ed.doc.blocks[0].type == BLOCK_CODE, "block stays CODE while inside the fence");
+ CHECK(text_eq(&ed.doc.blocks[0], "int main(void) {\n return 0;\n}"), "embedded newlines preserved, closing fence stripped");
+ CHECK(ed.doc.count == 2 && ed.doc.blocks[1].type == BLOCK_PARAGRAPH, "closing ``` exits to a new paragraph");
+ editor_free(&ed);
+}
+
+static void test_utf8_backspace(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "kaf\xc3\xa9"); /* "café" */
+ CHECK(ed.doc.blocks[0].text.len == 5, "café is 5 bytes (é is 2 bytes in UTF-8)");
+ editor_backspace(&ed);
+ CHECK(text_eq(&ed.doc.blocks[0], "kaf"), "backspace removes the whole 'é' codepoint, not one byte");
+ editor_free(&ed);
+}
+
+static void test_inline_parse(void) {
+ const char *s = "a **b** c *d* e `f` g [h](i)";
+ InlineRunList runs;
+ inline_parse(s, (int)strlen(s), &runs);
+
+ int haveBold = 0, haveItalic = 0, haveCode = 0, haveLink = 0;
+ for (int i = 0; i < runs.count; i++) {
+ InlineRun *r = &runs.runs[i];
+ if (r->kind == RUN_BOLD && r->contentEnd - r->contentStart == 1 && s[r->contentStart] == 'b') haveBold = 1;
+ if (r->kind == RUN_ITALIC && s[r->contentStart] == 'd') haveItalic = 1;
+ if (r->kind == RUN_CODE && s[r->contentStart] == 'f') haveCode = 1;
+ if (r->kind == RUN_LINK && s[r->contentStart] == 'h') haveLink = 1;
+ }
+ CHECK(haveBold, "inline_parse finds **bold**");
+ CHECK(haveItalic, "inline_parse finds *italic*");
+ CHECK(haveCode, "inline_parse finds `code`");
+ CHECK(haveLink, "inline_parse finds [link](url)");
+ inline_runs_free(&runs);
+}
+
+static void test_save_load_roundtrip(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "# Titel");
+ editor_enter(&ed);
+ type_str(&ed, "En **fet** rad med \xc3\xa5\xc3\xa4\xc3\xb6.");
+ editor_enter(&ed);
+ type_str(&ed, "> Citat");
+ editor_enter(&ed); /* continues the blockquote with a new, empty quote block */
+ editor_enter(&ed); /* Enter on an empty quote item exits back to a paragraph */
+ type_str(&ed, "- Punkt");
+
+ CHECK(document_save_file(&ed.doc, "test_roundtrip.md"), "save succeeds");
+
+ EditorState ed2;
+ editor_init(&ed2);
+ CHECK(editor_load(&ed2, "test_roundtrip.md"), "load succeeds");
+ CHECK(ed2.doc.count == 4, "round-tripped document has 4 blocks");
+ CHECK(ed2.doc.blocks[0].type == BLOCK_H1 && text_eq(&ed2.doc.blocks[0], "Titel"), "H1 round-trips");
+ CHECK(ed2.doc.blocks[1].type == BLOCK_PARAGRAPH && text_eq(&ed2.doc.blocks[1], "En **fet** rad med \xc3\xa5\xc3\xa4\xc3\xb6."), "paragraph with inline markup and åäö round-trips byte-for-byte");
+ CHECK(ed2.doc.blocks[2].type == BLOCK_QUOTE && text_eq(&ed2.doc.blocks[2], "Citat"), "blockquote round-trips");
+ CHECK(ed2.doc.blocks[3].type == BLOCK_BULLET && text_eq(&ed2.doc.blocks[3], "Punkt"), "bullet round-trips");
+
+ editor_free(&ed);
+ editor_free(&ed2);
+}
+
+static void test_document_clone_deep_copy(void) {
+ Document doc;
+ document_init(&doc);
+ sb_insert(&doc.blocks[0].text, 0, "hello", 5);
+ document_insert_block(&doc, 1, BLOCK_H1, "world", 5);
+
+ Document clone;
+ document_clone(&clone, &doc);
+ CHECK(clone.count == 2, "clone has the same block count");
+ CHECK(text_eq(&clone.blocks[0], "hello") && text_eq(&clone.blocks[1], "world"), "clone has matching text");
+ CHECK(clone.blocks[1].type == BLOCK_H1, "clone has matching block types");
+
+ sb_insert(&doc.blocks[0].text, 5, "XXX", 3); /* mutate the original after cloning */
+ CHECK(text_eq(&clone.blocks[0], "hello"), "clone is a deep copy, unaffected by mutating the original afterward");
+
+ document_free(&doc);
+ document_free(&clone);
+}
+
+static void test_document_compute_stats(void) {
+ EditorState ed;
+ editor_init(&ed);
+ DocStats stats;
+ document_compute_stats(&ed.doc, &stats);
+ CHECK(stats.byteSize == 1, "a fresh empty document serializes to a single newline byte");
+ CHECK(stats.lineCount == 1, "a fresh empty document counts as 1 line");
+
+ type_str(&ed, "ab");
+ editor_enter(&ed);
+ type_str(&ed, "cd");
+ document_compute_stats(&ed.doc, &stats);
+ CHECK(stats.byteSize == 7, "two-paragraph doc serializes to \"ab\\n\\ncd\\n\" (7 bytes)");
+ CHECK(stats.lineCount == 3, "the blank separator line between blocks counts too (matches wc -l)");
+ editor_free(&ed);
+}
+
+static void test_undo_typing_coalesces(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "abc");
+ CHECK(editor_undo(&ed), "undo has history after typing");
+ CHECK(text_eq(&ed.doc.blocks[0], ""), "one undo removes all of a continuously-typed run at once");
+ CHECK(!editor_undo(&ed), "a second undo finds nothing left");
+ editor_free(&ed);
+}
+
+static void test_undo_kind_switch_starts_new_group(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "abc");
+ editor_backspace(&ed);
+ editor_backspace(&ed);
+ CHECK(text_eq(&ed.doc.blocks[0], "a"), "typing 'abc' then two backspaces leaves 'a'");
+ CHECK(editor_undo(&ed), "undo #1");
+ CHECK(text_eq(&ed.doc.blocks[0], "abc"), "undo #1 undoes both backspaces together, but not the typing before them");
+ CHECK(editor_undo(&ed), "undo #2");
+ CHECK(text_eq(&ed.doc.blocks[0], ""), "undo #2 undoes the typed 'abc' as its own group");
+ editor_free(&ed);
+}
+
+static void test_undo_enter_is_own_step(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "ab");
+ editor_enter(&ed);
+ type_str(&ed, "cd");
+ CHECK(ed.doc.count == 2 && text_eq(&ed.doc.blocks[1], "cd"), "'ab' / Enter / 'cd' produced two blocks");
+
+ CHECK(editor_undo(&ed), "undo #1");
+ CHECK(ed.doc.count == 2 && text_eq(&ed.doc.blocks[1], ""), "undo #1 removes only the typed 'cd', the Enter split survives");
+ CHECK(editor_undo(&ed), "undo #2");
+ CHECK(ed.doc.count == 1 && text_eq(&ed.doc.blocks[0], "ab"), "undo #2 undoes the Enter split on its own, merging back to one block");
+ CHECK(editor_undo(&ed), "undo #3");
+ CHECK(ed.doc.count == 1 && text_eq(&ed.doc.blocks[0], ""), "undo #3 removes the typed 'ab'");
+ CHECK(!editor_undo(&ed), "no more history");
+ editor_free(&ed);
+}
+
+static void test_undo_redo_roundtrip(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "abc");
+ CHECK(editor_undo(&ed), "undo the typed 'abc'");
+ CHECK(text_eq(&ed.doc.blocks[0], ""), "back to empty");
+ CHECK(editor_redo(&ed), "redo restores it");
+ CHECK(text_eq(&ed.doc.blocks[0], "abc"), "redo brings back 'abc'");
+ CHECK(!editor_redo(&ed), "nothing left to redo");
+
+ type_str(&ed, "d");
+ CHECK(text_eq(&ed.doc.blocks[0], "abcd"), "typing after redo appends at the right cursor position");
+ CHECK(!editor_redo(&ed), "a genuinely new edit clears the redo stack");
+ editor_free(&ed);
+}
+
+static void test_snapshot_stack_cap_eviction(void) {
+ SnapshotStack s;
+ snapshot_stack_init(&s, 3);
+
+ Document doc;
+ document_init(&doc);
+ for (int i = 0; i < 5; i++) {
+ sb_clear(&doc.blocks[0].text);
+ char buf[8];
+ int n = snprintf(buf, sizeof buf, "%d", i);
+ sb_append(&doc.blocks[0].text, buf, n);
+ snapshot_stack_push(&s, &doc, 0, i);
+ }
+ CHECK(s.count == 3, "a cap-3 stack holds only 3 entries after 5 pushes");
+
+ Snapshot top;
+ CHECK(snapshot_stack_pop(&s, &top) && top.cursorOffset == 4, "most recent push is on top");
+ document_free(&top.doc);
+ CHECK(snapshot_stack_pop(&s, &top) && top.cursorOffset == 3, "next most recent is below it");
+ document_free(&top.doc);
+ CHECK(snapshot_stack_pop(&s, &top) && top.cursorOffset == 2, "oldest surviving entry is #2 (0 and 1 were evicted)");
+ document_free(&top.doc);
+ CHECK(!snapshot_stack_pop(&s, &top), "stack is now empty");
+
+ document_free(&doc);
+ snapshot_stack_free(&s);
+}
+
+static void test_selection_range_normalization(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "ab");
+ editor_enter(&ed);
+ type_str(&ed, "cd");
+
+ ed.cursorBlock = 0; ed.cursorOffset = 1;
+ ed.selAnchorBlock = 1; ed.selAnchorOffset = 1;
+ CHECK(editor_has_selection(&ed), "anchor != cursor counts as a selection");
+
+ int sb, so, eb, eo;
+ editor_selection_range(&ed, &sb, &so, &eb, &eo);
+ CHECK(sb == 0 && so == 1 && eb == 1 && eo == 1, "a backward selection (anchor after cursor) normalizes to forward document order");
+ editor_free(&ed);
+}
+
+static void test_selection_to_text_across_blocks(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "hello");
+ editor_enter(&ed);
+ type_str(&ed, "world");
+ editor_enter(&ed);
+ type_str(&ed, "again");
+
+ int len;
+ CHECK(editor_selection_to_text(&ed, &len) == NULL, "no active selection yields NULL");
+
+ ed.selAnchorBlock = 0; ed.selAnchorOffset = 3;
+ ed.cursorBlock = 2; ed.cursorOffset = 2;
+ char *text = editor_selection_to_text(&ed, &len);
+ CHECK(text != NULL, "selection_to_text returns text once a selection exists");
+ CHECK(text && strcmp(text, "lo\nworld\nag") == 0, "cross-block selection joins blocks with '\\n'");
+ free(text);
+ editor_free(&ed);
+}
+
+static void test_selection_replace_on_type(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "hello world");
+ ed.selAnchorBlock = 0; ed.selAnchorOffset = 0;
+ ed.cursorBlock = 0; ed.cursorOffset = 5;
+ editor_insert_utf8(&ed, "HI", 2);
+ CHECK(text_eq(&ed.doc.blocks[0], "HI world"), "typing over a selection replaces it");
+ CHECK(!editor_has_selection(&ed), "selection collapses after the replace");
+ CHECK(ed.cursorOffset == 2, "cursor lands right after the inserted text");
+ editor_free(&ed);
+}
+
+static void test_selection_replace_on_backspace_across_blocks(void) {
+ EditorState ed;
+ editor_init(&ed);
+ type_str(&ed, "## Heading");
+ editor_enter(&ed);
+ type_str(&ed, "world");
+ CHECK(ed.doc.count == 2 && ed.doc.blocks[0].type == BLOCK_H2, "setup: H2 'Heading' then paragraph 'world'");
+
+ ed.selAnchorBlock = 0; ed.selAnchorOffset = 3;
+ ed.cursorBlock = 1; ed.cursorOffset = 2;
+ editor_backspace(&ed);
+
+ CHECK(ed.doc.count == 1, "backspace over a cross-block selection merges into a single block");
+ CHECK(ed.doc.blocks[0].type == BLOCK_H2, "the merged block keeps the start block's type");
+ CHECK(text_eq(&ed.doc.blocks[0], "Hearld"), "merged text is start-before-selection + end-after-selection");
+ CHECK(!editor_has_selection(&ed), "selection is cleared after the merge");
+ editor_free(&ed);
+}
+
+static void test_config_parse_line(void) {
+ Config cfg = {0};
+
+ CHECK(!config_parse_line("# a comment", 11, &cfg), "a comment line returns false");
+ CHECK(!config_parse_line("", 0, &cfg), "a blank line returns false");
+ CHECK(!config_parse_line(" ", 3, &cfg), "a whitespace-only line returns false");
+
+ const char *l1 = "text_color = #1a2b3c";
+ CHECK(config_parse_line(l1, (int)strlen(l1), &cfg), "a valid hex color line returns true");
+ CHECK(cfg.textColor.r == 0x1a && cfg.textColor.g == 0x2b && cfg.textColor.b == 0x3c && cfg.textColor.a == 255,
+ "text_color parses each hex byte correctly");
+
+ const char *l2 = "bg_color=#ffffff";
+ CHECK(config_parse_line(l2, (int)strlen(l2), &cfg), "no spaces around '=' still parses");
+ CHECK(cfg.bgColor.r == 255 && cfg.bgColor.g == 255 && cfg.bgColor.b == 255, "bg_color parsed as white");
+
+ Clay_Color before = cfg.textColor;
+ const char *l3 = "text_color = not-a-color";
+ CHECK(!config_parse_line(l3, (int)strlen(l3), &cfg), "an invalid hex value returns false");
+ CHECK(cfg.textColor.r == before.r && cfg.textColor.g == before.g && cfg.textColor.b == before.b,
+ "an invalid color line leaves the previous value untouched");
+
+ const char *l4 = "unknown_key = foo";
+ CHECK(!config_parse_line(l4, (int)strlen(l4), &cfg), "an unrecognized key returns false and does not crash");
+
+ const char *l5 = "font_sans_bold = /some/path.ttf";
+ CHECK(config_parse_line(l5, (int)strlen(l5), &cfg), "a font-path key returns true");
+ CHECK(cfg.fontPaths[FONT_SANS_BOLD] != NULL && strcmp(cfg.fontPaths[FONT_SANS_BOLD], "/some/path.ttf") == 0,
+ "the font path is copied into the matching slot");
+
+ const char *l6 = "text_color = #a1b2c3\r";
+ CHECK(config_parse_line(l6, (int)strlen(l6), &cfg), "a trailing \\r is trimmed and the line still parses");
+ CHECK(cfg.textColor.r == 0xa1, "value from the \\r-trimmed line is correct");
+
+ config_free(&cfg);
+}
+
+int main(void) {
+ test_heading_autoformat();
+ test_bullet_list_continuation();
+ test_numbered_list_renumbers_on_save();
+ test_backspace_demotes_then_merges();
+ test_code_fence();
+ test_utf8_backspace();
+ test_inline_parse();
+ test_save_load_roundtrip();
+ test_document_clone_deep_copy();
+ test_document_compute_stats();
+ test_undo_typing_coalesces();
+ test_undo_kind_switch_starts_new_group();
+ test_undo_enter_is_own_step();
+ test_undo_redo_roundtrip();
+ test_snapshot_stack_cap_eviction();
+ test_selection_range_normalization();
+ test_selection_to_text_across_blocks();
+ test_selection_replace_on_type();
+ test_selection_replace_on_backspace_across_blocks();
+ test_config_parse_line();
+
+ printf("\n%s (%d failure%s)\n", g_failures == 0 ? "ALL PASS" : "SOME FAILED", g_failures, g_failures == 1 ? "" : "s");
+ return g_failures == 0 ? 0 : 1;
+}
diff --git a/src/theme.c b/src/theme.c
new file mode 100644
index 0000000..307f4c0
--- /dev/null
+++ b/src/theme.c
@@ -0,0 +1,9 @@
+#include "theme.h"
+
+Theme g_theme;
+
+void theme_init_defaults(void) {
+ g_theme.bg = DEFAULT_COL_BG;
+ g_theme.text = DEFAULT_COL_TEXT;
+ g_theme.cursor = DEFAULT_COL_CURSOR;
+}
diff --git a/src/theme.h b/src/theme.h
new file mode 100644
index 0000000..16e0636
--- /dev/null
+++ b/src/theme.h
@@ -0,0 +1,57 @@
+#ifndef HUSH_THEME_H
+#define HUSH_THEME_H
+
+#include "clay.h"
+
+/* bg/text/cursor are runtime-configurable (see Theme below, populated from the user's config
+ file); these three macros are only their built-in defaults. Every other color here stays a
+ compile-time constant -- intentionally out of config-file scope. */
+#define DEFAULT_COL_BG (Clay_Color){253, 253, 251, 255}
+#define DEFAULT_COL_TEXT (Clay_Color){31, 35, 40, 255}
+#define DEFAULT_COL_CURSOR (Clay_Color){31, 35, 40, 255}
+
+typedef struct {
+ Clay_Color bg;
+ Clay_Color text;
+ Clay_Color cursor;
+} Theme;
+
+extern Theme g_theme;
+void theme_init_defaults(void);
+
+#define COL_TEXT_QUOTE (Clay_Color){90, 97, 105, 255}
+#define COL_MARKER (Clay_Color){178, 178, 172, 255}
+#define COL_LINK (Clay_Color){37, 99, 235, 255}
+#define COL_CODE_BG (Clay_Color){240, 240, 236, 255}
+#define COL_CODE_TEXT (Clay_Color){160, 40, 90, 255}
+#define COL_CODEBLOCK_BG (Clay_Color){247, 247, 244, 255}
+#define COL_CODEBLOCK_TEXT (Clay_Color){55, 55, 60, 255}
+#define COL_QUOTE_BORDER (Clay_Color){206, 206, 200, 255}
+#define COL_SCROLLBAR (Clay_Color){210, 210, 205, 180}
+#define COL_SELECTION (Clay_Color){96, 165, 250, 90}
+#define COL_STATUSBAR_BG (Clay_Color){241, 241, 237, 255}
+#define COL_STATUSBAR_TEXT (Clay_Color){120, 126, 133, 255}
+#define COL_MODAL_SCRIM (Clay_Color){0, 0, 0, 120}
+#define COL_MODAL_BORDER (Clay_Color){206, 206, 200, 255}
+#define COL_BUTTON_HOVER (Clay_Color){235, 235, 231, 255}
+
+#define FS_H1 28
+#define FS_H2 23
+#define FS_H3 19
+#define FS_BODY 16
+#define FS_CODE 15
+#define FS_STATUSBAR 12
+#define FS_MODAL 15
+
+#define LINE_HEIGHT_MULT 1.5f
+
+#define CONTENT_MAX_WIDTH 760.0f
+#define CONTENT_SIDE_PADDING 32.0f
+#define BLOCK_GAP 4
+#define LIST_INDENT 26
+#define QUOTE_INDENT 14
+
+#define STATUSBAR_HEIGHT 24
+#define STATUSBAR_PADDING 10
+
+#endif
diff --git a/src/undo.c b/src/undo.c
new file mode 100644
index 0000000..4802d20
--- /dev/null
+++ b/src/undo.c
@@ -0,0 +1,82 @@
+#include "undo.h"
+#include <stdlib.h>
+#include <string.h>
+
+void snapshot_stack_init(SnapshotStack *s, int limit) {
+ s->items = NULL;
+ s->count = 0;
+ s->cap = 0;
+ s->limit = limit;
+}
+
+void snapshot_stack_clear(SnapshotStack *s) {
+ for (int i = 0; i < s->count; i++) document_free(&s->items[i].doc);
+ s->count = 0;
+}
+
+void snapshot_stack_free(SnapshotStack *s) {
+ snapshot_stack_clear(s);
+ free(s->items);
+ s->items = NULL;
+ s->cap = 0;
+}
+
+void snapshot_stack_push(SnapshotStack *s, const Document *doc, int cursorBlock, int cursorOffset) {
+ if (s->count == s->cap) {
+ int newCap = s->cap > 0 ? s->cap * 2 : 8;
+ s->items = realloc(s->items, (size_t)newCap * sizeof(Snapshot));
+ s->cap = newCap;
+ }
+ Snapshot *slot = &s->items[s->count++];
+ document_clone(&slot->doc, doc);
+ slot->cursorBlock = cursorBlock;
+ slot->cursorOffset = cursorOffset;
+
+ if (s->count > s->limit) {
+ /* Evict the oldest entry, shifting the rest down. */
+ document_free(&s->items[0].doc);
+ memmove(&s->items[0], &s->items[1], (size_t)(s->count - 1) * sizeof(Snapshot));
+ s->count--;
+ }
+}
+
+bool snapshot_stack_pop(SnapshotStack *s, Snapshot *out) {
+ if (s->count == 0) return false;
+ *out = s->items[--s->count];
+ return true;
+}
+
+void undo_manager_init(UndoManager *m, int limit) {
+ snapshot_stack_init(&m->undo, limit);
+ snapshot_stack_init(&m->redo, limit);
+ m->lastKind = EDIT_NONE;
+ m->lastBlock = -1;
+ m->lastOffset = -1;
+}
+
+void undo_manager_free(UndoManager *m) {
+ snapshot_stack_free(&m->undo);
+ snapshot_stack_free(&m->redo);
+}
+
+void undo_manager_reset(UndoManager *m) {
+ snapshot_stack_clear(&m->undo);
+ snapshot_stack_clear(&m->redo);
+ m->lastKind = EDIT_NONE;
+ m->lastBlock = -1;
+ m->lastOffset = -1;
+}
+
+void undo_manager_before_edit(UndoManager *m, EditKind kind, const Document *doc, int cursorBlock, int cursorOffset) {
+ bool coalesce = (kind == m->lastKind) && (cursorBlock == m->lastBlock) && (cursorOffset == m->lastOffset);
+ if (!coalesce) {
+ snapshot_stack_push(&m->undo, doc, cursorBlock, cursorOffset);
+ snapshot_stack_clear(&m->redo);
+ }
+}
+
+void undo_manager_after_edit(UndoManager *m, EditKind kind, int cursorBlock, int cursorOffset) {
+ m->lastKind = kind;
+ m->lastBlock = cursorBlock;
+ m->lastOffset = cursorOffset;
+}
diff --git a/src/undo.h b/src/undo.h
new file mode 100644
index 0000000..8041016
--- /dev/null
+++ b/src/undo.h
@@ -0,0 +1,47 @@
+#ifndef HUSH_UNDO_H
+#define HUSH_UNDO_H
+
+#include "document.h"
+#include <stdbool.h>
+
+typedef struct {
+ Document doc;
+ int cursorBlock, cursorOffset;
+} Snapshot;
+
+typedef struct {
+ Snapshot *items;
+ int count, cap;
+ int limit; /* max entries kept; oldest evicted beyond this */
+} SnapshotStack;
+
+void snapshot_stack_init(SnapshotStack *s, int limit);
+void snapshot_stack_free(SnapshotStack *s);
+void snapshot_stack_clear(SnapshotStack *s);
+void snapshot_stack_push(SnapshotStack *s, const Document *doc, int cursorBlock, int cursorOffset);
+/* Pops the most recent snapshot into *out (transfers Document ownership -- caller must
+ eventually document_free it). Returns false if the stack is empty. */
+bool snapshot_stack_pop(SnapshotStack *s, Snapshot *out);
+
+typedef enum { EDIT_NONE, EDIT_TYPE, EDIT_BACKSPACE, EDIT_DELETE_FWD, EDIT_ENTER, EDIT_PASTE } EditKind;
+
+typedef struct {
+ SnapshotStack undo, redo;
+ EditKind lastKind;
+ int lastBlock, lastOffset;
+} UndoManager;
+
+void undo_manager_init(UndoManager *m, int limit);
+void undo_manager_free(UndoManager *m);
+/* Clears both stacks and coalescing state -- call when a brand new document is loaded. */
+void undo_manager_reset(UndoManager *m);
+
+/* Call BEFORE mutating the document. Pushes a pre-edit snapshot (and clears the redo stack)
+ unless this edit coalesces with the immediately preceding one (same kind, cursor unmoved
+ since the last edit). */
+void undo_manager_before_edit(UndoManager *m, EditKind kind, const Document *doc, int cursorBlock, int cursorOffset);
+/* Call AFTER mutating, with the new cursor position, so the next call's coalescing check has
+ something to compare against. */
+void undo_manager_after_edit(UndoManager *m, EditKind kind, int cursorBlock, int cursorOffset);
+
+#endif
diff --git a/src/utf8.h b/src/utf8.h
new file mode 100644
index 0000000..4d14bee
--- /dev/null
+++ b/src/utf8.h
@@ -0,0 +1,24 @@
+#ifndef HUSH_UTF8_H
+#define HUSH_UTF8_H
+
+/* Byte length of the UTF-8 codepoint starting at text[pos], clamped so it never runs past `len`. */
+static inline int utf8_next_len(const char *text, int pos, int len) {
+ unsigned char c = (unsigned char)text[pos];
+ int n;
+ if ((c & 0x80) == 0x00) n = 1;
+ else if ((c & 0xE0) == 0xC0) n = 2;
+ else if ((c & 0xF0) == 0xE0) n = 3;
+ else if ((c & 0xF8) == 0xF0) n = 4;
+ else n = 1;
+ if (pos + n > len) n = len - pos;
+ return n;
+}
+
+/* Byte offset of the start of the codepoint immediately before `pos`. */
+static inline int utf8_prev_start(const char *text, int pos) {
+ int p = pos - 1;
+ while (p > 0 && ((unsigned char)text[p] & 0xC0) == 0x80) p--;
+ return p;
+}
+
+#endif
diff --git a/src/wrap.c b/src/wrap.c
new file mode 100644
index 0000000..aef6885
--- /dev/null
+++ b/src/wrap.c
@@ -0,0 +1,147 @@
+#include "wrap.h"
+#include <stdlib.h>
+
+typedef struct {
+ int runIndex;
+ int start, end;
+ float width;
+ int isSpace;
+} Atom;
+
+typedef struct {
+ int atomStart, atomEnd; /* [atomStart, atomEnd) into the atoms array */
+} Unit;
+
+void wrap_result_init(WrapResult *wr) {
+ wr->lines = NULL;
+ wr->count = 0;
+ wr->cap = 0;
+}
+
+void wrap_result_free(WrapResult *wr) {
+ for (int i = 0; i < wr->count; i++) free(wr->lines[i].segs);
+ free(wr->lines);
+ wr->lines = NULL;
+ wr->count = 0;
+ wr->cap = 0;
+}
+
+static WrapLine *push_line(WrapResult *wr) {
+ if (wr->count >= wr->cap) {
+ wr->cap = wr->cap > 0 ? wr->cap * 2 : 4;
+ wr->lines = realloc(wr->lines, (size_t)wr->cap * sizeof(WrapLine));
+ }
+ WrapLine *l = &wr->lines[wr->count++];
+ l->segs = NULL;
+ l->count = 0;
+ l->cap = 0;
+ return l;
+}
+
+static void push_seg(WrapLine *l, int runIndex, int start, int end, float width) {
+ if (l->count >= l->cap) {
+ l->cap = l->cap > 0 ? l->cap * 2 : 4;
+ l->segs = realloc(l->segs, (size_t)l->cap * sizeof(WrapSegment));
+ }
+ WrapSegment *s = &l->segs[l->count++];
+ s->runIndex = runIndex;
+ s->start = start;
+ s->end = end;
+ s->width = width;
+}
+
+static void finalize_line(WrapResult *out, const Atom *atoms, int atomStart, int atomEnd) {
+ WrapLine *line = push_line(out);
+ if (atomEnd <= atomStart) return; /* empty line, no segments */
+ int segRun = atoms[atomStart].runIndex;
+ int segStart = atoms[atomStart].start;
+ int segEnd = atoms[atomStart].end;
+ float segWidth = atoms[atomStart].width;
+ for (int i = atomStart + 1; i < atomEnd; i++) {
+ if (atoms[i].runIndex == segRun) {
+ segEnd = atoms[i].end;
+ segWidth += atoms[i].width;
+ } else {
+ push_seg(line, segRun, segStart, segEnd, segWidth);
+ segRun = atoms[i].runIndex;
+ segStart = atoms[i].start;
+ segEnd = atoms[i].end;
+ segWidth = atoms[i].width;
+ }
+ }
+ push_seg(line, segRun, segStart, segEnd, segWidth);
+}
+
+void wrap_layout(const char *text, const WrapRunSpan *runs, int runCount, float maxWidth,
+ WrapMeasureFn measure, void *measureCtx, WrapResult *out) {
+ wrap_result_init(out);
+
+ /* Step A: split each run's text into word/space atoms. */
+ Atom *atoms = NULL;
+ int atomCount = 0, atomCap = 0;
+ for (int r = 0; r < runCount; r++) {
+ int i = runs[r].start;
+ int end = runs[r].end;
+ while (i < end) {
+ int isSpace = (text[i] == ' ');
+ int j = i + 1;
+ while (j < end && (text[j] == ' ') == isSpace) j++;
+ if (atomCount >= atomCap) {
+ atomCap = atomCap > 0 ? atomCap * 2 : 16;
+ atoms = realloc(atoms, (size_t)atomCap * sizeof(Atom));
+ }
+ Atom *a = &atoms[atomCount++];
+ a->runIndex = r;
+ a->start = i;
+ a->end = j;
+ a->isSpace = isSpace;
+ a->width = measure(measureCtx, r, text, i, j - i);
+ i = j;
+ }
+ }
+
+ if (atomCount == 0) {
+ finalize_line(out, atoms, 0, 0);
+ free(atoms);
+ return;
+ }
+
+ /* Step B: group atoms into unbreakable units (word plus its trailing space, if any). */
+ Unit *units = NULL;
+ int unitCount = 0, unitCap = 0;
+ int a = 0;
+ while (a < atomCount) {
+ int unitStart = a;
+ while (a < atomCount) {
+ int wasSpace = atoms[a].isSpace;
+ a++;
+ if (wasSpace) break;
+ }
+ if (unitCount >= unitCap) {
+ unitCap = unitCap > 0 ? unitCap * 2 : 16;
+ units = realloc(units, (size_t)unitCap * sizeof(Unit));
+ }
+ units[unitCount].atomStart = unitStart;
+ units[unitCount].atomEnd = a;
+ unitCount++;
+ }
+
+ /* Step C: greedily pack units onto lines. */
+ int lineStartUnit = 0;
+ float lineWidth = 0.0f;
+ for (int u = 0; u < unitCount; u++) {
+ float unitWidth = 0.0f;
+ for (int i = units[u].atomStart; i < units[u].atomEnd; i++) unitWidth += atoms[i].width;
+
+ if (u > lineStartUnit && lineWidth + unitWidth > maxWidth) {
+ finalize_line(out, atoms, units[lineStartUnit].atomStart, units[u - 1].atomEnd);
+ lineStartUnit = u;
+ lineWidth = 0.0f;
+ }
+ lineWidth += unitWidth;
+ }
+ finalize_line(out, atoms, units[lineStartUnit].atomStart, units[unitCount - 1].atomEnd);
+
+ free(units);
+ free(atoms);
+}
diff --git a/src/wrap.h b/src/wrap.h
new file mode 100644
index 0000000..3514db7
--- /dev/null
+++ b/src/wrap.h
@@ -0,0 +1,38 @@
+#ifndef HUSH_WRAP_H
+#define HUSH_WRAP_H
+
+/* A contiguous byte range belonging to one styled render run, in source order. */
+typedef struct {
+ int start, end;
+} WrapRunSpan;
+
+typedef float (*WrapMeasureFn)(void *ctx, int runIndex, const char *text, int start, int len);
+
+/* One styled piece of text placed on a single visual line. `runIndex` refers back
+ into the WrapRunSpan array passed to wrap_layout (i.e. the caller's render-run list). */
+typedef struct {
+ int runIndex;
+ int start, end;
+ float width;
+} WrapSegment;
+
+typedef struct {
+ WrapSegment *segs;
+ int count, cap;
+} WrapLine;
+
+typedef struct {
+ WrapLine *lines;
+ int count, cap;
+} WrapResult;
+
+void wrap_result_init(WrapResult *wr);
+void wrap_result_free(WrapResult *wr);
+
+/* Greedily word-wraps `text` (using the styled run spans in `runs`) to fit within `maxWidth`.
+ Always produces at least one (possibly empty) line. Breaks only occur at space characters;
+ a run of atoms glued together with no space between them is never split across lines. */
+void wrap_layout(const char *text, const WrapRunSpan *runs, int runCount, float maxWidth,
+ WrapMeasureFn measure, void *measureCtx, WrapResult *out);
+
+#endif
diff --git a/vendor/clay.h b/vendor/clay.h
new file mode 100644
index 0000000..7d9cf4c
--- /dev/null
+++ b/vendor/clay.h
@@ -0,0 +1,5058 @@
+// VERSION: 0.14
+
+/*
+ NOTE: In order to use this library you must define
+ the following macro in exactly one file, _before_ including clay.h:
+
+ #define CLAY_IMPLEMENTATION
+ #include "clay.h"
+
+ See the examples folder for details.
+*/
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+// SIMD includes on supported platforms
+#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
+#include <emmintrin.h>
+#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__)
+#include <arm_neon.h>
+#endif
+#if __CLION_IDE__
+#define CLAY_IMPLEMENTATION
+#endif
+
+// -----------------------------------------
+// HEADER DECLARATIONS ---------------------
+// -----------------------------------------
+
+#ifndef CLAY_HEADER
+#define CLAY_HEADER
+
+#if !( \
+ (defined(__cplusplus) && __cplusplus >= 202002L) || \
+ (defined(__STDC__) && __STDC__ == 1 && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || \
+ defined(_MSC_VER) || \
+ defined(__OBJC__) \
+)
+#error "Clay requires C99, C++20, or MSVC"
+#endif
+
+#ifdef CLAY_WASM
+#define CLAY_WASM_EXPORT(name) __attribute__((export_name(name)))
+#else
+#define CLAY_WASM_EXPORT(null)
+#endif
+
+#ifdef CLAY_DLL
+#define CLAY_DLL_EXPORT __declspec(dllexport) __stdcall
+#else
+#define CLAY_DLL_EXPORT
+#endif
+
+// Public Macro API ------------------------
+
+#define CLAY__MAX(x, y) (((x) > (y)) ? (x) : (y))
+#define CLAY__MIN(x, y) (((x) < (y)) ? (x) : (y))
+
+#define CLAY_TEXT_CONFIG(...) __VA_ARGS__
+
+#define CLAY_BORDER_OUTSIDE(widthValue) {widthValue, widthValue, widthValue, widthValue, 0}
+
+#define CLAY_BORDER_ALL(widthValue) {widthValue, widthValue, widthValue, widthValue, widthValue}
+
+#define CLAY_CORNER_RADIUS(radius) (CLAY__INIT(Clay_CornerRadius) { radius, radius, radius, radius })
+
+#define CLAY_PADDING_ALL(padding) CLAY__CONFIG_WRAPPER(Clay_Padding, { padding, padding, padding, padding })
+
+#define CLAY_SIZING_FIT(...) (CLAY__INIT(Clay_SizingAxis) { .size = { .minMax = { __VA_ARGS__ } }, .type = CLAY__SIZING_TYPE_FIT })
+
+#define CLAY_SIZING_GROW(...) (CLAY__INIT(Clay_SizingAxis) { .size = { .minMax = { __VA_ARGS__ } }, .type = CLAY__SIZING_TYPE_GROW })
+
+#define CLAY_SIZING_FIXED(fixedSize) (CLAY__INIT(Clay_SizingAxis) { .size = { .minMax = { fixedSize, fixedSize } }, .type = CLAY__SIZING_TYPE_FIXED })
+
+#define CLAY_SIZING_PERCENT(percentOfParent) (CLAY__INIT(Clay_SizingAxis) { .size = { .percent = (percentOfParent) }, .type = CLAY__SIZING_TYPE_PERCENT })
+
+// Note: If a compile error led you here, you might be trying to use CLAY_ID with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SID instead.
+#define CLAY_ID(label) CLAY_SID(CLAY_STRING(label))
+
+#define CLAY_SID(label) Clay__HashString(label, 0)
+
+// Note: If a compile error led you here, you might be trying to use CLAY_IDI with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SIDI instead.
+#define CLAY_IDI(label, index) CLAY_SIDI(CLAY_STRING(label), index)
+
+#define CLAY_SIDI(label, index) Clay__HashStringWithOffset(label, index, 0)
+
+// Note: If a compile error led you here, you might be trying to use CLAY_ID_LOCAL with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SID_LOCAL instead.
+#define CLAY_ID_LOCAL(label) CLAY_SID_LOCAL(CLAY_STRING(label))
+
+#define CLAY_SID_LOCAL(label) Clay__HashString(label, Clay_GetOpenElementId())
+
+// Note: If a compile error led you here, you might be trying to use CLAY_IDI_LOCAL with something other than a string literal. To construct an ID with a dynamic string, use CLAY_SIDI_LOCAL instead.
+#define CLAY_IDI_LOCAL(label, index) CLAY_SIDI_LOCAL(CLAY_STRING(label), index)
+
+#define CLAY_SIDI_LOCAL(label, index) Clay__HashStringWithOffset(label, index, Clay_GetOpenElementId())
+
+#define CLAY__STRING_LENGTH(s) ((sizeof(s) / sizeof((s)[0])) - sizeof((s)[0]))
+
+#define CLAY__ENSURE_STRING_LITERAL(x) ("" x "")
+
+// Note: If an error led you here, it's because CLAY_STRING can only be used with string literals, i.e. CLAY_STRING("SomeString") and not CLAY_STRING(yourString)
+#define CLAY_STRING(string) (CLAY__INIT(Clay_String) { .isStaticallyAllocated = true, .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) })
+
+#define CLAY_STRING_CONST(string) { .isStaticallyAllocated = true, .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) }
+
+static uint8_t CLAY__ELEMENT_DEFINITION_LATCH;
+
+// GCC marks the above CLAY__ELEMENT_DEFINITION_LATCH as an unused variable for files that include clay.h but don't declare any layout
+// This is to suppress that warning
+static inline void Clay__SuppressUnusedLatchDefinitionVariableWarning(void) { (void) CLAY__ELEMENT_DEFINITION_LATCH; }
+
+// Publicly visible layout element macros -----------------------------------------------------
+
+/* This macro looks scary on the surface, but is actually quite simple.
+ It turns a macro call like this:
+
+ CLAY({
+ .id = CLAY_ID("Container"),
+ .backgroundColor = { 255, 200, 200, 255 }
+ }) {
+ ...children declared here
+ }
+
+ Into calls like this:
+
+ Clay__OpenElement();
+ Clay__ConfigureOpenElement((Clay_ElementDeclaration) {
+ .id = CLAY_ID("Container"),
+ .backgroundColor = { 255, 200, 200, 255 }
+ });
+ ...children declared here
+ Clay__CloseElement();
+
+ The for loop will only ever run a single iteration, putting Clay__CloseElement() in the increment of the loop
+ means that it will run after the body - where the children are declared. It just exists to make sure you don't forget
+ to call Clay_CloseElement().
+*/
+#define CLAY_AUTO_ID(...) \
+ for ( \
+ CLAY__ELEMENT_DEFINITION_LATCH = (Clay__OpenElement(), Clay__ConfigureOpenElement(CLAY__CONFIG_WRAPPER(Clay_ElementDeclaration, __VA_ARGS__)), 0); \
+ CLAY__ELEMENT_DEFINITION_LATCH < 1; \
+ CLAY__ELEMENT_DEFINITION_LATCH=1, Clay__CloseElement() \
+ )
+
+#define CLAY(id, ...) \
+ for ( \
+ CLAY__ELEMENT_DEFINITION_LATCH = (Clay__OpenElementWithId(id), Clay__ConfigureOpenElement(CLAY__CONFIG_WRAPPER(Clay_ElementDeclaration, __VA_ARGS__)), 0); \
+ CLAY__ELEMENT_DEFINITION_LATCH < 1; \
+ CLAY__ELEMENT_DEFINITION_LATCH=1, Clay__CloseElement() \
+ )
+
+// These macros exist to allow the CLAY() macro to be called both with an inline struct definition, such as
+// CLAY({ .id = something... });
+// As well as by passing a predefined declaration struct
+// Clay_ElementDeclaration declarationStruct = ...
+// CLAY(declarationStruct);
+#define CLAY__WRAPPER_TYPE(type) Clay__##type##Wrapper
+#define CLAY__WRAPPER_STRUCT(type) typedef struct { type wrapped; } CLAY__WRAPPER_TYPE(type)
+#define CLAY__CONFIG_WRAPPER(type, ...) (CLAY__INIT(CLAY__WRAPPER_TYPE(type)) { __VA_ARGS__ }).wrapped
+
+#define CLAY_TEXT(text, ...) Clay__OpenTextElement(text, CLAY__CONFIG_WRAPPER(Clay_TextElementConfig, __VA_ARGS__))
+
+#ifdef __cplusplus
+
+#define CLAY__INIT(type) type
+
+#define CLAY_PACKED_ENUM enum : uint8_t
+
+#define CLAY__DEFAULT_STRUCT {}
+
+#else
+
+#define CLAY__INIT(type) (type)
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define CLAY_PACKED_ENUM __pragma(pack(push, 1)) enum __pragma(pack(pop))
+#else
+#define CLAY_PACKED_ENUM enum __attribute__((__packed__))
+#endif
+
+#if __STDC_VERSION__ >= 202311L
+#define CLAY__DEFAULT_STRUCT {}
+#else
+#define CLAY__DEFAULT_STRUCT {0}
+#endif
+
+#endif // __cplusplus
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Utility Structs -------------------------
+
+// Note: Clay_String is not guaranteed to be null terminated. It may be if created from a literal C string,
+// but it is also used to represent slices.
+typedef struct Clay_String {
+ // Set this boolean to true if the char* data underlying this string will live for the entire lifetime of the program.
+ // This will automatically be set for strings created with CLAY_STRING, as the macro requires a string literal.
+ bool isStaticallyAllocated;
+ int32_t length;
+ // The underlying character memory. Note: this will not be copied and will not extend the lifetime of the underlying memory.
+ const char *chars;
+} Clay_String;
+
+// Clay_StringSlice is used to represent non owning string slices, and includes
+// a baseChars field which points to the string this slice is derived from.
+typedef struct Clay_StringSlice {
+ int32_t length;
+ const char *chars;
+ const char *baseChars; // The source string / char* that this slice was derived from
+} Clay_StringSlice;
+
+typedef struct Clay_Context Clay_Context;
+
+// Clay_Arena is a memory arena structure that is used by clay to manage its internal allocations.
+// Rather than creating it by hand, it's easier to use Clay_CreateArenaWithCapacityAndMemory()
+typedef struct Clay_Arena {
+ uintptr_t nextAllocation;
+ size_t capacity;
+ char *memory;
+} Clay_Arena;
+
+typedef struct Clay_Dimensions {
+ float width, height;
+} Clay_Dimensions;
+
+typedef struct Clay_Vector2 {
+ float x, y;
+} Clay_Vector2;
+
+// Internally clay conventionally represents colors as 0-255, but interpretation is up to the renderer.
+typedef struct Clay_Color {
+ float r, g, b, a;
+} Clay_Color;
+
+typedef struct Clay_BoundingBox {
+ float x, y, width, height;
+} Clay_BoundingBox;
+
+// Primarily created via the CLAY_ID(), CLAY_IDI(), CLAY_ID_LOCAL() and CLAY_IDI_LOCAL() macros.
+// Represents a hashed string ID used for identifying and finding specific clay UI elements, required
+// by functions such as Clay_PointerOver() and Clay_GetElementData().
+typedef struct Clay_ElementId {
+ uint32_t id; // The resulting hash generated from the other fields.
+ uint32_t offset; // A numerical offset applied after computing the hash from stringId.
+ uint32_t baseId; // A base hash value to start from, for example the parent element ID is used when calculating CLAY_ID_LOCAL().
+ Clay_String stringId; // The string id to hash.
+} Clay_ElementId;
+
+// A sized array of Clay_ElementId.
+typedef struct
+{
+ int32_t capacity;
+ int32_t length;
+ Clay_ElementId *internalArray;
+} Clay_ElementIdArray;
+
+// Controls the "radius", or corner rounding of elements, including rectangles, borders and images.
+// The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+typedef struct Clay_CornerRadius {
+ float topLeft;
+ float topRight;
+ float bottomLeft;
+ float bottomRight;
+} Clay_CornerRadius;
+
+// Element Configs ---------------------------
+
+// Controls the direction in which child elements will be automatically laid out.
+typedef CLAY_PACKED_ENUM {
+ // (Default) Lays out child elements from left to right with increasing x.
+ CLAY_LEFT_TO_RIGHT,
+ // Lays out child elements from top to bottom with increasing y.
+ CLAY_TOP_TO_BOTTOM,
+} Clay_LayoutDirection;
+
+// Controls the alignment along the x axis (horizontal) of child elements.
+typedef CLAY_PACKED_ENUM {
+ // (Default) Aligns child elements to the left hand side of this element, offset by padding.width.left
+ CLAY_ALIGN_X_LEFT,
+ // Aligns child elements to the right hand side of this element, offset by padding.width.right
+ CLAY_ALIGN_X_RIGHT,
+ // Aligns child elements horizontally to the center of this element
+ CLAY_ALIGN_X_CENTER,
+} Clay_LayoutAlignmentX;
+
+// Controls the alignment along the y axis (vertical) of child elements.
+typedef CLAY_PACKED_ENUM {
+ // (Default) Aligns child elements to the top of this element, offset by padding.width.top
+ CLAY_ALIGN_Y_TOP,
+ // Aligns child elements to the bottom of this element, offset by padding.width.bottom
+ CLAY_ALIGN_Y_BOTTOM,
+ // Aligns child elements vertically to the center of this element
+ CLAY_ALIGN_Y_CENTER,
+} Clay_LayoutAlignmentY;
+
+// Controls how the element takes up space inside its parent container.
+typedef CLAY_PACKED_ENUM {
+ // (default) Wraps tightly to the size of the element's contents.
+ CLAY__SIZING_TYPE_FIT,
+ // Expands along this axis to fill available space in the parent element, sharing it with other GROW elements.
+ CLAY__SIZING_TYPE_GROW,
+ // Expects 0-1 range. Clamps the axis size to a percent of the parent container's axis size minus padding and child gaps.
+ CLAY__SIZING_TYPE_PERCENT,
+ // Clamps the axis size to an exact size in pixels.
+ CLAY__SIZING_TYPE_FIXED,
+} Clay__SizingType;
+
+// Controls how child elements are aligned on each axis.
+typedef struct Clay_ChildAlignment {
+ Clay_LayoutAlignmentX x; // Controls alignment of children along the x axis.
+ Clay_LayoutAlignmentY y; // Controls alignment of children along the y axis.
+} Clay_ChildAlignment;
+
+// Controls the minimum and maximum size in pixels that this element is allowed to grow or shrink to,
+// overriding sizing types such as FIT or GROW.
+typedef struct Clay_SizingMinMax {
+ float min; // The smallest final size of the element on this axis will be this value in pixels.
+ float max; // The largest final size of the element on this axis will be this value in pixels.
+} Clay_SizingMinMax;
+
+// Controls the sizing of this element along one axis inside its parent container.
+typedef struct Clay_SizingAxis {
+ union {
+ Clay_SizingMinMax minMax; // Controls the minimum and maximum size in pixels that this element is allowed to grow or shrink to, overriding sizing types such as FIT or GROW.
+ float percent; // Expects 0-1 range. Clamps the axis size to a percent of the parent container's axis size minus padding and child gaps.
+ } size;
+ Clay__SizingType type; // Controls how the element takes up space inside its parent container.
+} Clay_SizingAxis;
+
+// Controls the sizing of this element along one axis inside its parent container.
+typedef struct Clay_Sizing {
+ Clay_SizingAxis width; // Controls the width sizing of the element, along the x axis.
+ Clay_SizingAxis height; // Controls the height sizing of the element, along the y axis.
+} Clay_Sizing;
+
+// Controls "padding" in pixels, which is a gap between the bounding box of this element and where its children
+// will be placed.
+typedef struct Clay_Padding {
+ uint16_t left;
+ uint16_t right;
+ uint16_t top;
+ uint16_t bottom;
+} Clay_Padding;
+
+CLAY__WRAPPER_STRUCT(Clay_Padding);
+
+// Controls various settings that affect the size and position of an element, as well as the sizes and positions
+// of any child elements.
+typedef struct Clay_LayoutConfig {
+ Clay_Sizing sizing; // Controls the sizing of this element inside it's parent container, including FIT, GROW, PERCENT and FIXED sizing.
+ Clay_Padding padding; // Controls "padding" in pixels, which is a gap between the bounding box of this element and where its children will be placed.
+ uint16_t childGap; // Controls the gap in pixels between child elements along the layout axis (horizontal gap for LEFT_TO_RIGHT, vertical gap for TOP_TO_BOTTOM).
+ Clay_ChildAlignment childAlignment; // Controls how child elements are aligned on each axis.
+ Clay_LayoutDirection layoutDirection; // Controls the direction in which child elements will be automatically laid out.
+} Clay_LayoutConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_LayoutConfig);
+
+extern Clay_LayoutConfig CLAY_LAYOUT_DEFAULT;
+
+// Controls how text "wraps", that is how it is broken into multiple lines when there is insufficient horizontal space.
+typedef CLAY_PACKED_ENUM {
+ // (default) breaks on whitespace characters.
+ CLAY_TEXT_WRAP_WORDS,
+ // Don't break on space characters, only on newlines.
+ CLAY_TEXT_WRAP_NEWLINES,
+ // Disable text wrapping entirely.
+ CLAY_TEXT_WRAP_NONE,
+} Clay_TextElementConfigWrapMode;
+
+// Controls how wrapped lines of text are horizontally aligned within the outer text bounding box.
+typedef CLAY_PACKED_ENUM {
+ // (default) Horizontally aligns wrapped lines of text to the left hand side of their bounding box.
+ CLAY_TEXT_ALIGN_LEFT,
+ // Horizontally aligns wrapped lines of text to the center of their bounding box.
+ CLAY_TEXT_ALIGN_CENTER,
+ // Horizontally aligns wrapped lines of text to the right hand side of their bounding box.
+ CLAY_TEXT_ALIGN_RIGHT,
+} Clay_TextAlignment;
+
+// Controls various functionality related to text elements.
+typedef struct Clay_TextElementConfig {
+ // A pointer that will be transparently passed through to the resulting render command.
+ void *userData;
+ // The RGBA color of the font to render, conventionally specified as 0-255.
+ Clay_Color textColor;
+ // An integer transparently passed to Clay_MeasureText to identify the font to use.
+ // The debug view will pass fontId = 0 for its internal text.
+ uint16_t fontId;
+ // Controls the size of the font. Handled by the function provided to Clay_MeasureText.
+ uint16_t fontSize;
+ // Controls extra horizontal spacing between characters. Handled by the function provided to Clay_MeasureText.
+ uint16_t letterSpacing;
+ // Controls additional vertical space between wrapped lines of text.
+ uint16_t lineHeight;
+ // Controls how text "wraps", that is how it is broken into multiple lines when there is insufficient horizontal space.
+ // CLAY_TEXT_WRAP_WORDS (default) breaks on whitespace characters.
+ // CLAY_TEXT_WRAP_NEWLINES doesn't break on space characters, only on newlines.
+ // CLAY_TEXT_WRAP_NONE disables wrapping entirely.
+ Clay_TextElementConfigWrapMode wrapMode;
+ // Controls how wrapped lines of text are horizontally aligned within the outer text bounding box.
+ // CLAY_TEXT_ALIGN_LEFT (default) - Horizontally aligns wrapped lines of text to the left hand side of their bounding box.
+ // CLAY_TEXT_ALIGN_CENTER - Horizontally aligns wrapped lines of text to the center of their bounding box.
+ // CLAY_TEXT_ALIGN_RIGHT - Horizontally aligns wrapped lines of text to the right hand side of their bounding box.
+ Clay_TextAlignment textAlignment;
+} Clay_TextElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_TextElementConfig);
+
+// Aspect Ratio --------------------------------
+
+// Controls various settings related to aspect ratio scaling element.
+typedef struct Clay_AspectRatioElementConfig {
+ float aspectRatio; // A float representing the target "Aspect ratio" for an element, which is its final width divided by its final height.
+} Clay_AspectRatioElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_AspectRatioElementConfig);
+
+// Image --------------------------------
+
+// Controls various settings related to image elements.
+typedef struct Clay_ImageElementConfig {
+ void* imageData; // A transparent pointer used to pass image data through to the renderer.
+} Clay_ImageElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_ImageElementConfig);
+
+// Floating -----------------------------
+
+// Controls where a floating element is offset relative to its parent element.
+// Note: see https://github.com/user-attachments/assets/b8c6dfaa-c1b1-41a4-be55-013473e4a6ce for a visual explanation.
+typedef CLAY_PACKED_ENUM {
+ CLAY_ATTACH_POINT_LEFT_TOP,
+ CLAY_ATTACH_POINT_LEFT_CENTER,
+ CLAY_ATTACH_POINT_LEFT_BOTTOM,
+ CLAY_ATTACH_POINT_CENTER_TOP,
+ CLAY_ATTACH_POINT_CENTER_CENTER,
+ CLAY_ATTACH_POINT_CENTER_BOTTOM,
+ CLAY_ATTACH_POINT_RIGHT_TOP,
+ CLAY_ATTACH_POINT_RIGHT_CENTER,
+ CLAY_ATTACH_POINT_RIGHT_BOTTOM,
+} Clay_FloatingAttachPointType;
+
+// Controls where a floating element is offset relative to its parent element.
+typedef struct Clay_FloatingAttachPoints {
+ Clay_FloatingAttachPointType element; // Controls the origin point on a floating element that attaches to its parent.
+ Clay_FloatingAttachPointType parent; // Controls the origin point on the parent element that the floating element attaches to.
+} Clay_FloatingAttachPoints;
+
+// Controls how mouse pointer events like hover and click are captured or passed through to elements underneath a floating element.
+typedef CLAY_PACKED_ENUM {
+ // (default) "Capture" the pointer event and don't allow events like hover and click to pass through to elements underneath.
+ CLAY_POINTER_CAPTURE_MODE_CAPTURE,
+ // CLAY_POINTER_CAPTURE_MODE_PARENT, TODO pass pointer through to attached parent
+ // Transparently pass through pointer events like hover and click to elements underneath the floating element.
+ CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH,
+} Clay_PointerCaptureMode;
+
+// Controls which element a floating element is "attached" to (i.e. relative offset from).
+typedef CLAY_PACKED_ENUM {
+ // (default) Disables floating for this element.
+ CLAY_ATTACH_TO_NONE,
+ // Attaches this floating element to its parent, positioned based on the .attachPoints and .offset fields.
+ CLAY_ATTACH_TO_PARENT,
+ // Attaches this floating element to an element with a specific ID, specified with the .parentId field. positioned based on the .attachPoints and .offset fields.
+ CLAY_ATTACH_TO_ELEMENT_WITH_ID,
+ // Attaches this floating element to the root of the layout, which combined with the .offset field provides functionality similar to "absolute positioning".
+ CLAY_ATTACH_TO_ROOT,
+} Clay_FloatingAttachToElement;
+
+// Controls whether or not a floating element is clipped to the same clipping rectangle as the element it's attached to.
+typedef CLAY_PACKED_ENUM {
+ // (default) - The floating element does not inherit clipping.
+ CLAY_CLIP_TO_NONE,
+ // The floating element is clipped to the same clipping rectangle as the element it's attached to.
+ CLAY_CLIP_TO_ATTACHED_PARENT
+} Clay_FloatingClipToElement;
+
+// Controls various settings related to "floating" elements, which are elements that "float" above other elements, potentially overlapping their boundaries,
+// and not affecting the layout of sibling or parent elements.
+typedef struct Clay_FloatingElementConfig {
+ // Offsets this floating element by the provided x,y coordinates from its attachPoints.
+ Clay_Vector2 offset;
+ // Expands the boundaries of the outer floating element without affecting its children.
+ Clay_Dimensions expand;
+ // When used in conjunction with .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID, attaches this floating element to the element in the hierarchy with the provided ID.
+ // Hint: attach the ID to the other element with .id = CLAY_ID("yourId"), and specify the id the same way, with .parentId = CLAY_ID("yourId").id
+ uint32_t parentId;
+ // Controls the z index of this floating element and all its children. Floating elements are sorted in ascending z order before output.
+ // zIndex is also passed to the renderer for all elements contained within this floating element.
+ int16_t zIndex;
+ // Controls how mouse pointer events like hover and click are captured or passed through to elements underneath / behind a floating element.
+ // Enum is of the form CLAY_ATTACH_POINT_foo_bar. See Clay_FloatingAttachPoints for more details.
+ // Note: see <img src="https://github.com/user-attachments/assets/b8c6dfaa-c1b1-41a4-be55-013473e4a6ce />
+ // and <img src="https://github.com/user-attachments/assets/ebe75e0d-1904-46b0-982d-418f929d1516 /> for a visual explanation.
+ Clay_FloatingAttachPoints attachPoints;
+ // Controls how mouse pointer events like hover and click are captured or passed through to elements underneath a floating element.
+ // CLAY_POINTER_CAPTURE_MODE_CAPTURE (default) - "Capture" the pointer event and don't allow events like hover and click to pass through to elements underneath.
+ // CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH - Transparently pass through pointer events like hover and click to elements underneath the floating element.
+ Clay_PointerCaptureMode pointerCaptureMode;
+ // Controls which element a floating element is "attached" to (i.e. relative offset from).
+ // CLAY_ATTACH_TO_NONE (default) - Disables floating for this element.
+ // CLAY_ATTACH_TO_PARENT - Attaches this floating element to its parent, positioned based on the .attachPoints and .offset fields.
+ // CLAY_ATTACH_TO_ELEMENT_WITH_ID - Attaches this floating element to an element with a specific ID, specified with the .parentId field. positioned based on the .attachPoints and .offset fields.
+ // CLAY_ATTACH_TO_ROOT - Attaches this floating element to the root of the layout, which combined with the .offset field provides functionality similar to "absolute positioning".
+ Clay_FloatingAttachToElement attachTo;
+ // Controls whether or not a floating element is clipped to the same clipping rectangle as the element it's attached to.
+ // CLAY_CLIP_TO_NONE (default) - The floating element does not inherit clipping.
+ // CLAY_CLIP_TO_ATTACHED_PARENT - The floating element is clipped to the same clipping rectangle as the element it's attached to.
+ Clay_FloatingClipToElement clipTo;
+} Clay_FloatingElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_FloatingElementConfig);
+
+// Custom -----------------------------
+
+// Controls various settings related to custom elements.
+typedef struct Clay_CustomElementConfig {
+ // A transparent pointer through which you can pass custom data to the renderer.
+ // Generates CUSTOM render commands.
+ void* customData;
+} Clay_CustomElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_CustomElementConfig);
+
+// Scroll -----------------------------
+
+// Controls the axis on which an element switches to "scrolling", which clips the contents and allows scrolling in that direction.
+typedef struct Clay_ClipElementConfig {
+ bool horizontal; // Clip overflowing elements on the X axis.
+ bool vertical; // Clip overflowing elements on the Y axis.
+ Clay_Vector2 childOffset; // Offsets the x,y positions of all child elements. Used primarily for scrolling containers.
+} Clay_ClipElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_ClipElementConfig);
+
+// Border -----------------------------
+
+// Controls the widths of individual element borders.
+typedef struct Clay_BorderWidth {
+ uint16_t left;
+ uint16_t right;
+ uint16_t top;
+ uint16_t bottom;
+ // Creates borders between each child element, depending on the .layoutDirection.
+ // e.g. for LEFT_TO_RIGHT, borders will be vertical lines, and for TOP_TO_BOTTOM borders will be horizontal lines.
+ // .betweenChildren borders will result in individual RECTANGLE render commands being generated.
+ uint16_t betweenChildren;
+} Clay_BorderWidth;
+
+// Controls settings related to element borders.
+typedef struct Clay_BorderElementConfig {
+ Clay_Color color; // Controls the color of all borders with width > 0. Conventionally represented as 0-255, but interpretation is up to the renderer.
+ Clay_BorderWidth width; // Controls the widths of individual borders. At least one of these should be > 0 for a BORDER render command to be generated.
+} Clay_BorderElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_BorderElementConfig);
+
+typedef struct {
+ Clay_BoundingBox boundingBox;
+ Clay_Color backgroundColor;
+ Clay_Color overlayColor;
+ Clay_Color borderColor;
+ Clay_BorderWidth borderWidth;
+} Clay_TransitionData;
+
+typedef enum {
+ CLAY_TRANSITION_STATE_IDLE,
+ CLAY_TRANSITION_STATE_ENTERING,
+ CLAY_TRANSITION_STATE_TRANSITIONING,
+ CLAY_TRANSITION_STATE_EXITING,
+} Clay_TransitionState;
+
+typedef enum {
+ CLAY_TRANSITION_PROPERTY_NONE = 0,
+ CLAY_TRANSITION_PROPERTY_X = 1,
+ CLAY_TRANSITION_PROPERTY_Y = 2,
+ CLAY_TRANSITION_PROPERTY_POSITION = CLAY_TRANSITION_PROPERTY_X | CLAY_TRANSITION_PROPERTY_Y,
+ CLAY_TRANSITION_PROPERTY_WIDTH = 4,
+ CLAY_TRANSITION_PROPERTY_HEIGHT = 8,
+ CLAY_TRANSITION_PROPERTY_DIMENSIONS = CLAY_TRANSITION_PROPERTY_WIDTH | CLAY_TRANSITION_PROPERTY_HEIGHT,
+ CLAY_TRANSITION_PROPERTY_BOUNDING_BOX = CLAY_TRANSITION_PROPERTY_POSITION | CLAY_TRANSITION_PROPERTY_DIMENSIONS,
+ CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR = 16,
+ CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR = 32,
+ CLAY_TRANSITION_PROPERTY_CORNER_RADIUS = 64,
+ CLAY_TRANSITION_PROPERTY_BORDER_COLOR = 128,
+ CLAY_TRANSITION_PROPERTY_BORDER_WIDTH = 256,
+ CLAY_TRANSITION_PROPERTY_BORDER = CLAY_TRANSITION_PROPERTY_BORDER_COLOR | CLAY_TRANSITION_PROPERTY_BORDER_WIDTH
+} Clay_TransitionProperty;
+
+typedef struct {
+ Clay_TransitionState transitionState;
+ Clay_TransitionData initial;
+ Clay_TransitionData *current;
+ Clay_TransitionData target;
+ float elapsedTime;
+ float duration;
+ Clay_TransitionProperty properties;
+} Clay_TransitionCallbackArguments;
+
+typedef CLAY_PACKED_ENUM {
+ CLAY_TRANSITION_ENTER_SKIP_ON_FIRST_PARENT_FRAME,
+ CLAY_TRANSITION_ENTER_TRIGGER_ON_FIRST_PARENT_FRAME,
+} Clay_TransitionEnterTriggerType;
+
+typedef CLAY_PACKED_ENUM {
+ CLAY_TRANSITION_EXIT_SKIP_WHEN_PARENT_EXITS,
+ CLAY_TRANSITION_EXIT_TRIGGER_WHEN_PARENT_EXITS,
+} Clay_TransitionExitTriggerType;
+
+typedef CLAY_PACKED_ENUM {
+ CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION,
+ CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION,
+} Clay_TransitionInteractionHandlingType;
+
+typedef CLAY_PACKED_ENUM {
+ CLAY_EXIT_TRANSITION_ORDERING_UNDERNEATH_SIBLINGS,
+ CLAY_EXIT_TRANSITION_ORDERING_NATURAL_ORDER,
+ CLAY_EXIT_TRANSITION_ORDERING_ABOVE_SIBLINGS,
+} Clay_ExitTransitionSiblingOrdering;
+
+// Controls settings related to transitions
+typedef struct Clay_TransitionElementConfig {
+ bool (*handler)(Clay_TransitionCallbackArguments arguments);
+ float duration;
+ Clay_TransitionProperty properties;
+ Clay_TransitionInteractionHandlingType interactionHandling;
+ struct {
+ Clay_TransitionData (*setInitialState)(Clay_TransitionData targetState, Clay_TransitionProperty properties);
+ Clay_TransitionEnterTriggerType trigger;
+ } enter;
+ struct {
+ Clay_TransitionData (*setFinalState)(Clay_TransitionData initialState, Clay_TransitionProperty properties);
+ Clay_TransitionExitTriggerType trigger;
+ Clay_ExitTransitionSiblingOrdering siblingOrdering;
+ } exit;
+} Clay_TransitionElementConfig;
+
+CLAY__WRAPPER_STRUCT(Clay_TransitionElementConfig);
+
+// Render Command Data -----------------------------
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_TEXT
+typedef struct Clay_TextRenderData {
+ // A string slice containing the text to be rendered.
+ // Note: this is not guaranteed to be null terminated.
+ Clay_StringSlice stringContents;
+ // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+ Clay_Color textColor;
+ // An integer representing the font to use to render this text, transparently passed through from the text declaration.
+ uint16_t fontId;
+ uint16_t fontSize;
+ // Specifies the extra whitespace gap in pixels between each character.
+ uint16_t letterSpacing;
+ // The height of the bounding box for this line of text.
+ uint16_t lineHeight;
+} Clay_TextRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_RECTANGLE
+typedef struct Clay_RectangleRenderData {
+ // The solid background color to fill this rectangle with. Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+ Clay_Color backgroundColor;
+ // Controls the "radius", or corner rounding of elements, including rectangles, borders and images.
+ // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+ Clay_CornerRadius cornerRadius;
+} Clay_RectangleRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_IMAGE
+typedef struct Clay_ImageRenderData {
+ // The tint color for this image. Note that the default value is 0,0,0,0 and should likely be interpreted
+ // as "untinted".
+ // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+ Clay_Color backgroundColor;
+ // Controls the "radius", or corner rounding of this image.
+ // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+ Clay_CornerRadius cornerRadius;
+ // A pointer transparently passed through from the original element definition, typically used to represent image data.
+ void* imageData;
+} Clay_ImageRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_CUSTOM
+typedef struct Clay_CustomRenderData {
+ // Passed through from .backgroundColor in the original element declaration.
+ // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+ Clay_Color backgroundColor;
+ // Controls the "radius", or corner rounding of this custom element.
+ // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+ Clay_CornerRadius cornerRadius;
+ // A pointer transparently passed through from the original element definition.
+ void* customData;
+} Clay_CustomRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_START || commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_END
+typedef struct Clay_ClipRenderData {
+ bool horizontal;
+ bool vertical;
+} Clay_ClipRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START || commandType == CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END
+typedef struct Clay_OverlayColorRenderData {
+ Clay_Color color;
+} Clay_OverlayColorRenderData;
+
+// Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_BORDER
+typedef struct Clay_BorderRenderData {
+ // Controls a shared color for all this element's borders.
+ // Conventionally represented as 0-255 for each channel, but interpretation is up to the renderer.
+ Clay_Color color;
+ // Specifies the "radius", or corner rounding of this border element.
+ // The rounding is determined by drawing a circle inset into the element corner by (radius, radius) pixels.
+ Clay_CornerRadius cornerRadius;
+ // Controls individual border side widths.
+ Clay_BorderWidth width;
+} Clay_BorderRenderData;
+
+// A struct union containing data specific to this command's .commandType
+typedef union Clay_RenderData {
+ // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_RECTANGLE
+ Clay_RectangleRenderData rectangle;
+ // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_TEXT
+ Clay_TextRenderData text;
+ // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_IMAGE
+ Clay_ImageRenderData image;
+ // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_CUSTOM
+ Clay_CustomRenderData custom;
+ // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_BORDER
+ Clay_BorderRenderData border;
+ // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_START|END
+ Clay_ClipRenderData clip;
+ // Render command data when commandType == CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START|END
+ Clay_OverlayColorRenderData overlayColor;
+} Clay_RenderData;
+
+// Miscellaneous Structs & Enums ---------------------------------
+
+// Data representing the current internal state of a scrolling element.
+typedef struct Clay_ScrollContainerData {
+ // Note: This is a pointer to the real internal scroll position, mutating it may cause a change in final layout.
+ // Intended for use with external functionality that modifies scroll position, such as scroll bars or auto scrolling.
+ Clay_Vector2 *scrollPosition;
+ // The bounding box of the scroll element.
+ Clay_Dimensions scrollContainerDimensions;
+ // The outer dimensions of the inner scroll container content, including the padding of the parent scroll container.
+ Clay_Dimensions contentDimensions;
+ // The config that was originally passed to the clip element.
+ Clay_ClipElementConfig config;
+ // Indicates whether an actual scroll container matched the provided ID or if the default struct was returned.
+ bool found;
+} Clay_ScrollContainerData;
+
+// Bounding box and other data for a specific UI element.
+typedef struct Clay_ElementData {
+ // The rectangle that encloses this UI element, with the position relative to the root of the layout.
+ Clay_BoundingBox boundingBox;
+ // Indicates whether an actual Element matched the provided ID or if the default struct was returned.
+ bool found;
+} Clay_ElementData;
+
+// Used by renderers to determine specific handling for each render command.
+typedef CLAY_PACKED_ENUM {
+ // This command type should be skipped.
+ CLAY_RENDER_COMMAND_TYPE_NONE,
+ // The renderer should draw a solid color rectangle.
+ CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+ // The renderer should draw a colored border inset into the bounding box.
+ CLAY_RENDER_COMMAND_TYPE_BORDER,
+ // The renderer should draw text.
+ CLAY_RENDER_COMMAND_TYPE_TEXT,
+ // The renderer should draw an image.
+ CLAY_RENDER_COMMAND_TYPE_IMAGE,
+ // The renderer should begin clipping all future draw commands, only rendering content that falls within the provided boundingBox.
+ CLAY_RENDER_COMMAND_TYPE_SCISSOR_START,
+ // The renderer should finish any previously active clipping, and begin rendering elements in full again.
+ CLAY_RENDER_COMMAND_TYPE_SCISSOR_END,
+ // The renderer should begin performing a "color overlay" on all subsequent render commands until disabled again.
+ CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START,
+ // The renderer should disable any previously active "color overlay" and render elements with their standard colors again.
+ CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END,
+ // The renderer should provide a custom implementation for handling this render command based on its .customData
+ CLAY_RENDER_COMMAND_TYPE_CUSTOM,
+} Clay_RenderCommandType;
+
+typedef struct Clay_RenderCommand {
+ // A rectangular box that fully encloses this UI element, with the position relative to the root of the layout.
+ Clay_BoundingBox boundingBox;
+ // A struct union containing data specific to this command's commandType.
+ Clay_RenderData renderData;
+ // A pointer transparently passed through from the original element declaration.
+ void *userData;
+ // The id of this element, transparently passed through from the original element declaration.
+ uint32_t id;
+ // The z order required for drawing this command correctly.
+ // Note: the render command array is already sorted in ascending order, and will produce correct results if drawn in naive order.
+ // This field is intended for use in batching renderers for improved performance.
+ int16_t zIndex;
+ // Specifies how to handle rendering of this command.
+ // CLAY_RENDER_COMMAND_TYPE_RECTANGLE - The renderer should draw a solid color rectangle.
+ // CLAY_RENDER_COMMAND_TYPE_BORDER - The renderer should draw a colored border inset into the bounding box.
+ // CLAY_RENDER_COMMAND_TYPE_TEXT - The renderer should draw text.
+ // CLAY_RENDER_COMMAND_TYPE_IMAGE - The renderer should draw an image.
+ // CLAY_RENDER_COMMAND_TYPE_SCISSOR_START - The renderer should begin clipping all future draw commands, only rendering content that falls within the provided boundingBox.
+ // CLAY_RENDER_COMMAND_TYPE_SCISSOR_END - The renderer should finish any previously active clipping, and begin rendering elements in full again.
+ // CLAY_RENDER_COMMAND_TYPE_CUSTOM - The renderer should provide a custom implementation for handling this render command based on its .customData
+ Clay_RenderCommandType commandType;
+} Clay_RenderCommand;
+
+// A sized array of render commands.
+typedef struct Clay_RenderCommandArray {
+ // The underlying max capacity of the array, not necessarily all initialized.
+ int32_t capacity;
+ // The number of initialized elements in this array. Used for loops and iteration.
+ int32_t length;
+ // A pointer to the first element in the internal array.
+ Clay_RenderCommand* internalArray;
+} Clay_RenderCommandArray;
+
+// Represents the current state of interaction with clay this frame.
+typedef CLAY_PACKED_ENUM {
+ // A left mouse click, or touch occurred this frame.
+ CLAY_POINTER_DATA_PRESSED_THIS_FRAME,
+ // The left mouse button click or touch happened at some point in the past, and is still currently held down this frame.
+ CLAY_POINTER_DATA_PRESSED,
+ // The left mouse button click or touch was released this frame.
+ CLAY_POINTER_DATA_RELEASED_THIS_FRAME,
+ // The left mouse button click or touch is not currently down / was released at some point in the past.
+ CLAY_POINTER_DATA_RELEASED,
+} Clay_PointerDataInteractionState;
+
+// Information on the current state of pointer interactions this frame.
+typedef struct Clay_PointerData {
+ // The position of the mouse / touch / pointer relative to the root of the layout.
+ Clay_Vector2 position;
+ // Represents the current state of interaction with clay this frame.
+ // CLAY_POINTER_DATA_PRESSED_THIS_FRAME - A left mouse click, or touch occurred this frame.
+ // CLAY_POINTER_DATA_PRESSED - The left mouse button click or touch happened at some point in the past, and is still currently held down this frame.
+ // CLAY_POINTER_DATA_RELEASED_THIS_FRAME - The left mouse button click or touch was released this frame.
+ // CLAY_POINTER_DATA_RELEASED - The left mouse button click or touch is not currently down / was released at some point in the past.
+ Clay_PointerDataInteractionState state;
+} Clay_PointerData;
+
+typedef struct Clay_ElementDeclaration {
+ // Controls various settings that affect the size and position of an element, as well as the sizes and positions of any child elements.
+ Clay_LayoutConfig layout;
+ // Controls the background color of the resulting element.
+ // By convention specified as 0-255, but interpretation is up to the renderer.
+ // If no other config is specified, .backgroundColor will generate a RECTANGLE render command, otherwise it will be passed as a property to IMAGE or CUSTOM render commands.
+ Clay_Color backgroundColor;
+ // Perform an image editing style "Color Overlay" on this element and all its children, equivalent to
+ // glsl mix(elementColor, overlayColor.rgb, overlayColor.a)
+ Clay_Color overlayColor;
+ // Controls the "radius", or corner rounding of elements, including rectangles, borders and images.
+ Clay_CornerRadius cornerRadius;
+ // Controls settings related to aspect ratio scaling.
+ Clay_AspectRatioElementConfig aspectRatio;
+ // Controls settings related to image elements.
+ Clay_ImageElementConfig image;
+ // Controls whether and how an element "floats", which means it layers over the top of other elements in z order, and doesn't affect the position and size of siblings or parent elements.
+ // Note: in order to activate floating, .floating.attachTo must be set to something other than the default value.
+ Clay_FloatingElementConfig floating;
+ // Used to create CUSTOM render commands, usually to render element types not supported by Clay.
+ Clay_CustomElementConfig custom;
+ // Controls whether an element should clip its contents, as well as providing child x,y offset configuration for scrolling.
+ Clay_ClipElementConfig clip;
+ // Controls settings related to element borders, and will generate BORDER render commands.
+ Clay_BorderElementConfig border;
+ Clay_TransitionElementConfig transition;
+ // A pointer that will be transparently passed through to resulting render commands.
+ void *userData;
+} Clay_ElementDeclaration;
+
+CLAY__WRAPPER_STRUCT(Clay_ElementDeclaration);
+
+// Represents the type of error clay encountered while computing layout.
+typedef CLAY_PACKED_ENUM {
+ // A text measurement function wasn't provided using Clay_SetMeasureTextFunction(), or the provided function was null.
+ CLAY_ERROR_TYPE_TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED,
+ // Clay attempted to allocate its internal data structures but ran out of space.
+ // The arena passed to Clay_Initialize was created with a capacity smaller than that required by Clay_MinMemorySize().
+ CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED,
+ // Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxElementCount().
+ CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED,
+ // Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxMeasureTextCacheWordCount().
+ CLAY_ERROR_TYPE_TEXT_MEASUREMENT_CAPACITY_EXCEEDED,
+ // Two elements were declared with exactly the same ID within one layout.
+ CLAY_ERROR_TYPE_DUPLICATE_ID,
+ // A floating element was declared using CLAY_ATTACH_TO_ELEMENT_ID and either an invalid .parentId was provided or no element with the provided .parentId was found.
+ CLAY_ERROR_TYPE_FLOATING_CONTAINER_PARENT_NOT_FOUND,
+ // An element was declared that using CLAY_SIZING_PERCENT but the percentage value was over 1. Percentage values are expected to be in the 0-1 range.
+ CLAY_ERROR_TYPE_PERCENTAGE_OVER_1,
+ // Clay encountered an internal error. It would be wonderful if you could report this so we can fix it!
+ CLAY_ERROR_TYPE_INTERNAL_ERROR,
+ // Clay__OpenElement was called more times than Clay__CloseElement, so there were still remaining open elements when the layout ended.
+ CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE,
+ CLAY_ERROR_TYPE_HASH_MAP_CAPACITY_EXCEEDED
+} Clay_ErrorType;
+
+// Data to identify the error that clay has encountered.
+typedef struct Clay_ErrorData {
+ // Represents the type of error clay encountered while computing layout.
+ // CLAY_ERROR_TYPE_TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED - A text measurement function wasn't provided using Clay_SetMeasureTextFunction(), or the provided function was null.
+ // CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED - Clay attempted to allocate its internal data structures but ran out of space. The arena passed to Clay_Initialize was created with a capacity smaller than that required by Clay_MinMemorySize().
+ // CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED - Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxElementCount().
+ // CLAY_ERROR_TYPE_TEXT_MEASUREMENT_CAPACITY_EXCEEDED - Clay ran out of capacity in its internal array for storing elements. This limit can be increased with Clay_SetMaxMeasureTextCacheWordCount().
+ // CLAY_ERROR_TYPE_DUPLICATE_ID - Two elements were declared with exactly the same ID within one layout.
+ // CLAY_ERROR_TYPE_FLOATING_CONTAINER_PARENT_NOT_FOUND - A floating element was declared using CLAY_ATTACH_TO_ELEMENT_ID and either an invalid .parentId was provided or no element with the provided .parentId was found.
+ // CLAY_ERROR_TYPE_PERCENTAGE_OVER_1 - An element was declared that using CLAY_SIZING_PERCENT but the percentage value was over 1. Percentage values are expected to be in the 0-1 range.
+ // CLAY_ERROR_TYPE_INTERNAL_ERROR - Clay encountered an internal error. It would be wonderful if you could report this so we can fix it!
+ // CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE - Clay__OpenElement was called more times than Clay__CloseElement, so there were still remaining open elements when the layout ended.
+ // CLAY_ERROR_TYPE_HASH_MAP_CAPACITY_EXCEEDED - Clay ran out of capacity in its internal hash map for storing element IDs -> elements. This limit can be increased with Clay_SetMaxElementCount().
+ Clay_ErrorType errorType;
+ // A string containing human-readable error text that explains the error in more detail.
+ Clay_String errorText;
+ // A transparent pointer passed through from when the error handler was first provided.
+ void *userData;
+} Clay_ErrorData;
+
+// A wrapper struct around Clay's error handler function.
+typedef struct {
+ // A user provided function to call when Clay encounters an error during layout.
+ void (*errorHandlerFunction)(Clay_ErrorData errorText);
+ // A pointer that will be transparently passed through to the error handler when it is called.
+ void *userData;
+} Clay_ErrorHandler;
+
+// Function Forward Declarations ---------------------------------
+
+// Public API functions ------------------------------------------
+
+// Returns the size, in bytes, of the minimum amount of memory Clay requires to operate at its current settings.
+CLAY_DLL_EXPORT uint32_t Clay_MinMemorySize(void);
+// Creates an arena for clay to use for its internal allocations, given a certain capacity in bytes and a pointer to an allocation of at least that size.
+// Intended to be used with Clay_MinMemorySize in the following way:
+// uint32_t minMemoryRequired = Clay_MinMemorySize();
+// Clay_Arena clayMemory = Clay_CreateArenaWithCapacityAndMemory(minMemoryRequired, malloc(minMemoryRequired));
+CLAY_DLL_EXPORT Clay_Arena Clay_CreateArenaWithCapacityAndMemory(size_t capacity, void *memory);
+// Sets the state of the "pointer" (i.e. the mouse or touch) in Clay's internal data. Used for detecting and responding to mouse events in the debug view,
+// as well as for Clay_Hovered() and scroll element handling.
+CLAY_DLL_EXPORT void Clay_SetPointerState(Clay_Vector2 position, bool pointerDown);
+// Returns the state of the "pointer" (i.e. the mouse or touch) which was set via Clay_SetPointerState().
+CLAY_DLL_EXPORT Clay_PointerData Clay_GetPointerState(void);
+// Initialize Clay's internal arena and setup required data before layout can begin. Only needs to be called once.
+// - arena can be created using Clay_CreateArenaWithCapacityAndMemory()
+// - layoutDimensions are the initial bounding dimensions of the layout (i.e. the screen width and height for a full screen layout)
+// - errorHandler is used by Clay to inform you if something has gone wrong in configuration or layout.
+CLAY_DLL_EXPORT Clay_Context* Clay_Initialize(Clay_Arena arena, Clay_Dimensions layoutDimensions, Clay_ErrorHandler errorHandler);
+// Returns the Context that clay is currently using. Used when using multiple instances of clay simultaneously.
+CLAY_DLL_EXPORT Clay_Context* Clay_GetCurrentContext(void);
+// Sets the context that clay will use to compute the layout.
+// Used to restore a context saved from Clay_GetCurrentContext when using multiple instances of clay simultaneously.
+CLAY_DLL_EXPORT void Clay_SetCurrentContext(Clay_Context* context);
+// Updates the state of Clay's internal scroll data, updating scroll content positions if scrollDelta is non zero, and progressing momentum scrolling.
+// - enableDragScrolling when set to true will enable mobile device like "touch drag" scroll of scroll containers, including momentum scrolling after the touch has ended.
+// - scrollDelta is the amount to scroll this frame on each axis in pixels.
+// - deltaTime is the time in seconds since the last "frame" (scroll update)
+CLAY_DLL_EXPORT void Clay_UpdateScrollContainers(bool enableDragScrolling, Clay_Vector2 scrollDelta, float deltaTime);
+// Returns the internally stored scroll offset for the currently open element.
+// Generally intended for use with clip elements to create scrolling containers.
+CLAY_DLL_EXPORT Clay_Vector2 Clay_GetScrollOffset(void);
+// Updates the layout dimensions in response to the window or outer container being resized.
+CLAY_DLL_EXPORT void Clay_SetLayoutDimensions(Clay_Dimensions dimensions);
+// Returns the current dimensions set by Clay_SetLayoutDimensions.
+CLAY_DLL_EXPORT Clay_Dimensions Clay_GetLayoutDimensions(void);
+// Called before starting any layout declarations.
+CLAY_DLL_EXPORT void Clay_BeginLayout(void);
+// Called when all layout declarations are finished.
+// Computes the layout and generates and returns the array of render commands to draw.
+CLAY_DLL_EXPORT Clay_RenderCommandArray Clay_EndLayout(float deltaTime);
+// Gets the ID of the currently open element, useful for retrieving IDs generated by CLAY_AUTO_ID()
+CLAY_DLL_EXPORT uint32_t Clay_GetOpenElementId(void);
+// Calculates a hash ID from the given idString.
+// Generally only used for dynamic strings when CLAY_ID("stringLiteral") can't be used.
+CLAY_DLL_EXPORT Clay_ElementId Clay_GetElementId(Clay_String idString);
+// Calculates a hash ID from the given idString and index.
+// - index is used to avoid constructing dynamic ID strings in loops.
+// Generally only used for dynamic strings when CLAY_IDI("stringLiteral", index) can't be used.
+CLAY_DLL_EXPORT Clay_ElementId Clay_GetElementIdWithIndex(Clay_String idString, uint32_t index);
+// Returns layout data such as the final calculated bounding box for an element with a given ID.
+// The returned Clay_ElementData contains a `found` bool that will be true if an element with the provided ID was found.
+// This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings.
+CLAY_DLL_EXPORT Clay_ElementData Clay_GetElementData(Clay_ElementId id);
+// Returns true if the pointer position provided by Clay_SetPointerState is within the current element's bounding box.
+// Works during element declaration, e.g. CLAY({ .backgroundColor = Clay_Hovered() ? BLUE : RED });
+CLAY_DLL_EXPORT bool Clay_Hovered(void);
+// Bind a callback that will be called when the pointer position provided by Clay_SetPointerState is within the current element's bounding box.
+// - onHoverFunction is a function pointer to a user defined function.
+// - userData is a pointer that will be transparently passed through when the onHoverFunction is called.
+CLAY_DLL_EXPORT void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerData, void *userData), void *userData);
+// An imperative function that returns true if the pointer position provided by Clay_SetPointerState is within the element with the provided ID's bounding box.
+// This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings.
+CLAY_DLL_EXPORT bool Clay_PointerOver(Clay_ElementId elementId);
+// Returns the array of element IDs that the pointer is currently over.
+CLAY_DLL_EXPORT Clay_ElementIdArray Clay_GetPointerOverIds(void);
+// Returns data representing the state of the scrolling element with the provided ID.
+// The returned Clay_ScrollContainerData contains a `found` bool that will be true if a scroll element was found with the provided ID.
+// An imperative function that returns true if the pointer position provided by Clay_SetPointerState is within the element with the provided ID's bounding box.
+// This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings.
+CLAY_DLL_EXPORT Clay_ScrollContainerData Clay_GetScrollContainerData(Clay_ElementId id);
+// Binds a callback function that Clay will call to determine the dimensions of a given string slice.
+// - measureTextFunction is a user provided function that adheres to the interface Clay_Dimensions (Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
+// - userData is a pointer that will be transparently passed through when the measureTextFunction is called.
+CLAY_DLL_EXPORT void Clay_SetMeasureTextFunction(Clay_Dimensions (*measureTextFunction)(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData), void *userData);
+// Experimental - Used in cases where Clay needs to integrate with a system that manages its own scrolling containers externally.
+// Please reach out if you plan to use this function, as it may be subject to change.
+CLAY_DLL_EXPORT void Clay_SetQueryScrollOffsetFunction(Clay_Vector2 (*queryScrollOffsetFunction)(uint32_t elementId, void *userData), void *userData);
+// A bounds-checked "get" function for the Clay_RenderCommandArray returned from Clay_EndLayout().
+CLAY_DLL_EXPORT Clay_RenderCommand * Clay_RenderCommandArray_Get(Clay_RenderCommandArray* array, int32_t index);
+// Enables and disables Clay's internal debug tools.
+// This state is retained and does not need to be set each frame.
+CLAY_DLL_EXPORT void Clay_SetDebugModeEnabled(bool enabled);
+// Returns true if Clay's internal debug tools are currently enabled.
+CLAY_DLL_EXPORT bool Clay_IsDebugModeEnabled(void);
+// Enables and disables visibility culling. By default, Clay will not generate render commands for elements whose bounding box is entirely outside the screen.
+CLAY_DLL_EXPORT void Clay_SetCullingEnabled(bool enabled);
+// Returns the maximum number of UI elements supported by Clay's current configuration.
+CLAY_DLL_EXPORT int32_t Clay_GetMaxElementCount(void);
+// Modifies the maximum number of UI elements supported by Clay's current configuration.
+// This may require reallocating additional memory, and re-calling Clay_Initialize();
+CLAY_DLL_EXPORT void Clay_SetMaxElementCount(int32_t maxElementCount);
+// Returns the maximum number of measured "words" (whitespace seperated runs of characters) that Clay can store in its internal text measurement cache.
+CLAY_DLL_EXPORT int32_t Clay_GetMaxMeasureTextCacheWordCount(void);
+// Modifies the maximum number of measured "words" (whitespace seperated runs of characters) that Clay can store in its internal text measurement cache.
+// This may require reallocating additional memory, and re-calling Clay_Initialize();
+CLAY_DLL_EXPORT void Clay_SetMaxMeasureTextCacheWordCount(int32_t maxMeasureTextCacheWordCount);
+// Resets Clay's internal text measurement cache. Useful if font mappings have changed or fonts have been reloaded.
+CLAY_DLL_EXPORT void Clay_ResetMeasureTextCache(void);
+// A built in transition function that uses the "Ease Out" curve
+CLAY_DLL_EXPORT bool Clay_EaseOut(Clay_TransitionCallbackArguments arguments);
+
+// Internal API functions required by macros ----------------------
+
+CLAY_DLL_EXPORT void Clay__OpenElement(void);
+CLAY_DLL_EXPORT void Clay__OpenElementWithId(Clay_ElementId elementId);
+CLAY_DLL_EXPORT void Clay__ConfigureOpenElement(const Clay_ElementDeclaration config);
+CLAY_DLL_EXPORT void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *config);
+CLAY_DLL_EXPORT void Clay__CloseElement(void);
+CLAY_DLL_EXPORT Clay_ElementId Clay__HashString(Clay_String key, uint32_t seed);
+CLAY_DLL_EXPORT Clay_ElementId Clay__HashStringWithOffset(Clay_String key, uint32_t offset, uint32_t seed);
+CLAY_DLL_EXPORT void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig textConfig);
+
+extern Clay_Color Clay__debugViewHighlightColor;
+extern uint32_t Clay__debugViewWidth;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // CLAY_HEADER
+
+// -----------------------------------------
+// IMPLEMENTATION --------------------------
+// -----------------------------------------
+#ifdef CLAY_IMPLEMENTATION
+#undef CLAY_IMPLEMENTATION
+
+#ifndef CLAY__NULL
+#define CLAY__NULL 0
+#endif
+
+#ifndef CLAY__MAXFLOAT
+#define CLAY__MAXFLOAT 3.40282346638528859812e+38F
+#endif
+
+Clay_LayoutConfig CLAY_LAYOUT_DEFAULT = CLAY__DEFAULT_STRUCT;
+
+Clay_Color Clay__Color_DEFAULT = CLAY__DEFAULT_STRUCT;
+Clay_CornerRadius Clay__CornerRadius_DEFAULT = CLAY__DEFAULT_STRUCT;
+Clay_BorderWidth Clay__BorderWidth_DEFAULT = CLAY__DEFAULT_STRUCT;
+
+// The below functions define array bounds checking and convenience functions for a provided type.
+#define CLAY__ARRAY_DEFINE_FUNCTIONS(typeName, arrayName) \
+ \
+typedef struct \
+{ \
+ int32_t length; \
+ typeName *internalArray; \
+} arrayName##Slice; \
+ \
+typeName typeName##_DEFAULT = CLAY__DEFAULT_STRUCT; \
+ \
+arrayName arrayName##_Allocate_Arena(int32_t capacity, Clay_Arena *arena) { \
+ return CLAY__INIT(arrayName){.capacity = capacity, .length = 0, \
+ .internalArray = (typeName *)Clay__Array_Allocate_Arena(capacity, sizeof(typeName), arena)}; \
+} \
+ \
+typeName *arrayName##_Get(arrayName *array, int32_t index) { \
+ return Clay__Array_RangeCheck(index, array->length) ? &array->internalArray[index] : &typeName##_DEFAULT; \
+} \
+ \
+typeName arrayName##_GetValue(arrayName *array, int32_t index) { \
+ return Clay__Array_RangeCheck(index, array->length) ? array->internalArray[index] : typeName##_DEFAULT; \
+} \
+ \
+typeName *arrayName##_GetCheckCapacity(arrayName *array, int32_t index) { \
+ return Clay__Array_RangeCheck(index, array->capacity) ? &array->internalArray[index] : &typeName##_DEFAULT; \
+} \
+ \
+typeName *arrayName##_Add(arrayName *array, typeName item) { \
+ if (Clay__Array_AddCapacityCheck(array->length, array->capacity)) { \
+ array->internalArray[array->length++] = item; \
+ return &array->internalArray[array->length - 1]; \
+ } \
+ return &typeName##_DEFAULT; \
+} \
+ \
+typeName *arrayName##Slice_Get(arrayName##Slice *slice, int32_t index) { \
+ return Clay__Array_RangeCheck(index, slice->length) ? &slice->internalArray[index] : &typeName##_DEFAULT; \
+} \
+ \
+typeName arrayName##_RemoveSwapback(arrayName *array, int32_t index) { \
+ if (Clay__Array_RangeCheck(index, array->length)) { \
+ array->length--; \
+ typeName removed = array->internalArray[index]; \
+ array->internalArray[index] = array->internalArray[array->length]; \
+ return removed; \
+ } \
+ return typeName##_DEFAULT; \
+} \
+ \
+typeName* arrayName##_Set(arrayName *array, int32_t index, typeName value) { \
+ if (Clay__Array_RangeCheck(index, array->capacity)) { \
+ array->internalArray[index] = value; \
+ array->length = index < array->length ? array->length : index + 1; \
+ return &array->internalArray[index];\
+ } \
+ return NULL;\
+} \
+ \
+typeName* arrayName##_Set_DontTouchLength(arrayName *array, int32_t index, typeName value) { \
+ if (Clay__Array_RangeCheck(index, array->capacity)) { \
+ array->internalArray[index] = value; \
+ return &array->internalArray[index];\
+ } \
+ return NULL;\
+} \
+
+#define CLAY__ARRAY_DEFINE(typeName, arrayName) \
+typedef struct \
+{ \
+ int32_t capacity; \
+ int32_t length; \
+ typeName *internalArray; \
+} arrayName; \
+ \
+CLAY__ARRAY_DEFINE_FUNCTIONS(typeName, arrayName) \
+
+Clay_Context *Clay__currentContext;
+int32_t Clay__defaultMaxElementCount = 8192;
+int32_t Clay__defaultMaxMeasureTextWordCacheCount = 16384;
+
+void Clay__ErrorHandlerFunctionDefault(Clay_ErrorData errorText) {
+ (void) errorText;
+}
+
+Clay_String CLAY__SPACECHAR = { .length = 1, .chars = " " };
+Clay_String CLAY__STRING_DEFAULT = { .length = 0, .chars = NULL };
+
+typedef struct {
+ bool maxElementsExceeded;
+ bool maxRenderCommandsExceeded;
+ bool maxTextMeasureCacheExceeded;
+ bool textMeasurementFunctionNotSet;
+ bool hashMapCapacityExceeded;
+} Clay_BooleanWarnings;
+
+typedef struct {
+ Clay_String baseMessage;
+ Clay_String dynamicMessage;
+} Clay__Warning;
+
+Clay__Warning CLAY__WARNING_DEFAULT = CLAY__DEFAULT_STRUCT;
+
+typedef struct {
+ int32_t capacity;
+ int32_t length;
+ Clay__Warning *internalArray;
+} Clay__WarningArray;
+
+Clay__WarningArray Clay__WarningArray_Allocate_Arena(int32_t capacity, Clay_Arena *arena);
+Clay__Warning *Clay__WarningArray_Add(Clay__WarningArray *array, Clay__Warning item);
+void* Clay__Array_Allocate_Arena(int32_t capacity, uint32_t itemSize, Clay_Arena *arena);
+bool Clay__Array_RangeCheck(int32_t index, int32_t length);
+bool Clay__Array_AddCapacityCheck(int32_t length, int32_t capacity);
+
+CLAY__ARRAY_DEFINE(bool, Clay__boolArray)
+CLAY__ARRAY_DEFINE(int32_t, Clay__int32_tArray)
+CLAY__ARRAY_DEFINE(char, Clay__charArray)
+CLAY__ARRAY_DEFINE_FUNCTIONS(Clay_ElementId, Clay_ElementIdArray)
+CLAY__ARRAY_DEFINE(Clay_String, Clay__StringArray)
+CLAY__ARRAY_DEFINE_FUNCTIONS(Clay_RenderCommand, Clay_RenderCommandArray)
+
+typedef struct {
+ Clay_Dimensions dimensions;
+ Clay_String line;
+} Clay__WrappedTextLine;
+
+CLAY__ARRAY_DEFINE(Clay__WrappedTextLine, Clay__WrappedTextLineArray)
+
+typedef struct {
+ Clay_String text;
+ Clay_Dimensions preferredDimensions;
+ Clay__WrappedTextLineArraySlice wrappedLines;
+} Clay__TextElementData;
+
+typedef struct {
+ int32_t *elements;
+ uint16_t length;
+} Clay__LayoutElementChildren;
+
+typedef struct Clay_LayoutElement {
+ Clay__LayoutElementChildren children;
+ Clay_Dimensions dimensions;
+ Clay_Dimensions minDimensions;
+ union {
+ Clay_ElementDeclaration config;
+ struct {
+ Clay_TextElementConfig textConfig;
+ Clay__TextElementData textElementData;
+ };
+ };
+ uint32_t id;
+ uint16_t floatingChildrenCount;
+ bool isTextElement;
+ // True if the element is currently in an exit transition, and is "synthetic"
+ // i.e. data was retained from previous frames
+ bool exiting;
+} Clay_LayoutElement;
+
+CLAY__ARRAY_DEFINE(Clay_LayoutElement, Clay_LayoutElementArray)
+
+typedef struct {
+ Clay_LayoutElement *layoutElement;
+ Clay_BoundingBox boundingBox;
+ Clay_Dimensions contentSize;
+ Clay_Vector2 scrollOrigin;
+ Clay_Vector2 pointerOrigin;
+ Clay_Vector2 scrollMomentum;
+ Clay_Vector2 scrollPosition;
+ Clay_Vector2 previousDelta;
+ float momentumTime;
+ uint32_t elementId;
+ bool openThisFrame;
+ bool pointerScrollActive;
+} Clay__ScrollContainerDataInternal;
+
+CLAY__ARRAY_DEFINE(Clay__ScrollContainerDataInternal, Clay__ScrollContainerDataInternalArray)
+
+// Data representing the current internal state of a transition element.
+typedef struct Clay__TransitionDataInternal {
+ Clay_TransitionData initialState;
+ Clay_TransitionData currentState;
+ Clay_TransitionData targetState;
+ Clay_LayoutElement* elementThisFrame;
+ Clay_Vector2 oldParentRelativePosition;
+ uint32_t elementId;
+ uint32_t parentId;
+ uint32_t siblingIndex;
+ float elapsedTime;
+ Clay_TransitionState state;
+ bool transitionOut;
+ bool reparented;
+ Clay_TransitionProperty activeProperties;
+} Clay__TransitionDataInternal;
+
+CLAY__ARRAY_DEFINE(Clay__TransitionDataInternal, Clay__TransitionDataInternalArray)
+
+typedef struct { // todo get this struct into a single cache line
+ Clay_BoundingBox boundingBox;
+ Clay_ElementId elementId;
+ Clay_LayoutElement* layoutElement;
+ void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData);
+ void *hoverFunctionUserData;
+ int32_t nextIndex;
+ uint32_t generation;
+ bool appearedThisFrame;
+ struct {
+ bool collision;
+ bool collapsed;
+ } debugData;
+} Clay_LayoutElementHashMapItem;
+
+CLAY__ARRAY_DEFINE(Clay_LayoutElementHashMapItem, Clay__LayoutElementHashMapItemArray)
+
+typedef struct {
+ int32_t startOffset;
+ int32_t length;
+ float width;
+ int32_t next;
+} Clay__MeasuredWord;
+
+CLAY__ARRAY_DEFINE(Clay__MeasuredWord, Clay__MeasuredWordArray)
+
+typedef struct {
+ Clay_Dimensions unwrappedDimensions;
+ int32_t measuredWordsStartIndex;
+ float minWidth;
+ bool containsNewlines;
+ // Hash map data
+ uint32_t id;
+ int32_t nextIndex;
+ uint32_t generation;
+} Clay__MeasureTextCacheItem;
+
+CLAY__ARRAY_DEFINE(Clay__MeasureTextCacheItem, Clay__MeasureTextCacheItemArray)
+
+typedef struct {
+ Clay_LayoutElement *layoutElement;
+ Clay_Vector2 position;
+ Clay_Vector2 nextChildOffset;
+ bool parentMovedThisFramed; // Used to relativise transitions
+} Clay__LayoutElementTreeNode;
+
+CLAY__ARRAY_DEFINE(Clay__LayoutElementTreeNode, Clay__LayoutElementTreeNodeArray)
+
+typedef struct {
+ int32_t layoutElementIndex;
+ uint32_t parentId; // This can be zero in the case of the root layout tree
+ uint32_t clipElementId; // This can be zero if there is no clip element
+ int16_t zIndex;
+ Clay_Vector2 pointerOffset; // Only used when scroll containers are managed externally
+} Clay__LayoutElementTreeRoot;
+
+CLAY__ARRAY_DEFINE(Clay__LayoutElementTreeRoot, Clay__LayoutElementTreeRootArray)
+
+struct Clay_Context {
+ int32_t maxElementCount;
+ int32_t maxMeasureTextCacheWordCount;
+ int32_t exitingElementsLength;
+ int32_t exitingElementsChildrenLength;
+ bool warningsEnabled;
+ bool rootResizedLastFrame;
+ Clay_ErrorHandler errorHandler;
+ Clay_BooleanWarnings booleanWarnings;
+ Clay__WarningArray warnings;
+
+ Clay_PointerData pointerInfo;
+ Clay_Dimensions layoutDimensions;
+ Clay_ElementId dynamicElementIndexBaseHash;
+ uint32_t dynamicElementIndex;
+ bool debugModeEnabled;
+ bool disableCulling;
+ bool externalScrollHandlingEnabled;
+ uint32_t debugSelectedElementId;
+ uint32_t generation;
+ uintptr_t arenaResetOffset;
+ void *measureTextUserData;
+ void *queryScrollOffsetUserData;
+ Clay_Arena internalArena;
+ // Layout Elements / Render Commands
+ Clay_LayoutElementArray layoutElements;
+ Clay_RenderCommandArray renderCommands;
+ Clay__int32_tArray openLayoutElementStack;
+ Clay__int32_tArray layoutElementChildren;
+ Clay__int32_tArray layoutElementChildrenBuffer;
+ Clay__int32_tArray reusableElementIndexBuffer;
+ Clay__int32_tArray layoutElementClipElementIds;
+ // Misc Data Structures
+ Clay__StringArray layoutElementIdStrings;
+ Clay__WrappedTextLineArray wrappedTextLines;
+ Clay__LayoutElementTreeNodeArray layoutElementTreeNodeArray1;
+ Clay__LayoutElementTreeRootArray layoutElementTreeRoots;
+ Clay__LayoutElementHashMapItemArray layoutElementsHashMapInternal;
+ Clay__int32_tArray layoutElementsHashMap;
+ Clay__int32_tArray layoutElementsHashMapFreeList;
+ Clay__MeasureTextCacheItemArray measureTextHashMapInternal;
+ Clay__int32_tArray measureTextHashMapInternalFreeList;
+ Clay__int32_tArray measureTextHashMap;
+ Clay__MeasuredWordArray measuredWords;
+ Clay__int32_tArray measuredWordsFreeList;
+ Clay__int32_tArray openClipElementStack;
+ Clay_ElementIdArray pointerOverIds;
+ Clay__ScrollContainerDataInternalArray scrollContainerDatas;
+ Clay__TransitionDataInternalArray transitionDatas;
+ Clay__boolArray treeNodeVisited;
+ Clay__charArray dynamicStringData;
+};
+
+Clay_Context* Clay__Context_Allocate_Arena(Clay_Arena *arena) {
+ size_t totalSizeBytes = sizeof(Clay_Context);
+ if (totalSizeBytes > arena->capacity)
+ {
+ return NULL;
+ }
+ arena->nextAllocation += totalSizeBytes;
+ return (Clay_Context*)(arena->memory);
+}
+
+Clay_String Clay__WriteStringToCharBuffer(Clay__charArray *buffer, Clay_String string) {
+ for (int32_t i = 0; i < string.length; i++) {
+ buffer->internalArray[buffer->length + i] = string.chars[i];
+ }
+ buffer->length += string.length;
+ return CLAY__INIT(Clay_String) { .length = string.length, .chars = (const char *)(buffer->internalArray + buffer->length - string.length) };
+}
+
+#ifdef CLAY_WASM
+ __attribute__((import_module("clay"), import_name("measureTextFunction"))) Clay_Dimensions Clay__MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
+ __attribute__((import_module("clay"), import_name("queryScrollOffsetFunction"))) Clay_Vector2 Clay__QueryScrollOffset(uint32_t elementId, void *userData);
+#else
+ Clay_Dimensions (*Clay__MeasureText)(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
+ Clay_Vector2 (*Clay__QueryScrollOffset)(uint32_t elementId, void *userData);
+#endif
+
+Clay_LayoutElement* Clay__GetOpenLayoutElement(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ return Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1));
+}
+
+Clay_LayoutElement* Clay__GetParentElement(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ return Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2));
+}
+
+uint32_t Clay__GetParentElementId(void) {
+ return Clay__GetParentElement()->id;
+}
+
+bool Clay__BorderHasAnyWidth(Clay_BorderElementConfig* borderConfig) {
+ return borderConfig->width.betweenChildren > 0 || borderConfig->width.left > 0 || borderConfig->width.right > 0 || borderConfig->width.top > 0 || borderConfig->width.bottom > 0;
+}
+
+Clay_ElementId Clay__HashNumber(const uint32_t offset, const uint32_t seed) {
+ uint32_t hash = seed;
+ hash += (offset + 48);
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+
+ hash += (hash << 3);
+ hash ^= (hash >> 11);
+ hash += (hash << 15);
+ return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = offset, .baseId = seed, .stringId = CLAY__STRING_DEFAULT }; // Reserve the hash result of zero as "null id"
+}
+
+Clay_ElementId Clay__HashString(Clay_String key, const uint32_t seed) {
+ uint32_t hash = seed;
+
+ for (int32_t i = 0; i < key.length; i++) {
+ hash += key.chars[i];
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+ }
+
+ hash += (hash << 3);
+ hash ^= (hash >> 11);
+ hash += (hash << 15);
+ return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = 0, .baseId = hash + 1, .stringId = key }; // Reserve the hash result of zero as "null id"
+}
+
+Clay_ElementId Clay__HashStringWithOffset(Clay_String key, const uint32_t offset, const uint32_t seed) {
+ uint32_t hash = 0;
+ uint32_t base = seed;
+
+ for (int32_t i = 0; i < key.length; i++) {
+ base += key.chars[i];
+ base += (base << 10);
+ base ^= (base >> 6);
+ }
+ hash = base;
+ hash += offset;
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+
+ hash += (hash << 3);
+ base += (base << 3);
+ hash ^= (hash >> 11);
+ base ^= (base >> 11);
+ hash += (hash << 15);
+ base += (base << 15);
+ return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = offset, .baseId = base + 1, .stringId = key }; // Reserve the hash result of zero as "null id"
+}
+
+#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
+static inline __m128i Clay__SIMDRotateLeft(__m128i x, int r) {
+ return _mm_or_si128(_mm_slli_epi64(x, r), _mm_srli_epi64(x, 64 - r));
+}
+
+static inline void Clay__SIMDARXMix(__m128i* a, __m128i* b) {
+ *a = _mm_add_epi64(*a, *b);
+ *b = _mm_xor_si128(Clay__SIMDRotateLeft(*b, 17), *a);
+}
+
+uint64_t Clay__HashData(const uint8_t* data, size_t length) {
+ // Pinched these constants from the BLAKE implementation
+ __m128i v0 = _mm_set1_epi64x(0x6a09e667f3bcc908ULL);
+ __m128i v1 = _mm_set1_epi64x(0xbb67ae8584caa73bULL);
+ __m128i v2 = _mm_set1_epi64x(0x3c6ef372fe94f82bULL);
+ __m128i v3 = _mm_set1_epi64x(0xa54ff53a5f1d36f1ULL);
+
+ uint8_t overflowBuffer[16] = { 0 }; // Temporary buffer for small inputs
+
+ while (length > 0) {
+ __m128i msg;
+ if (length >= 16) {
+ msg = _mm_loadu_si128((const __m128i*)data);
+ data += 16;
+ length -= 16;
+ }
+ else {
+ for (size_t i = 0; i < length; i++) {
+ overflowBuffer[i] = data[i];
+ }
+ msg = _mm_loadu_si128((const __m128i*)overflowBuffer);
+ length = 0;
+ }
+
+ v0 = _mm_xor_si128(v0, msg);
+ Clay__SIMDARXMix(&v0, &v1);
+ Clay__SIMDARXMix(&v2, &v3);
+
+ v0 = _mm_add_epi64(v0, v2);
+ v1 = _mm_add_epi64(v1, v3);
+ }
+
+ Clay__SIMDARXMix(&v0, &v1);
+ Clay__SIMDARXMix(&v2, &v3);
+ v0 = _mm_add_epi64(v0, v2);
+ v1 = _mm_add_epi64(v1, v3);
+ v0 = _mm_add_epi64(v0, v1);
+
+ uint64_t result[2];
+ _mm_storeu_si128((__m128i*)result, v0);
+
+ return result[0] ^ result[1];
+}
+#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__)
+static inline void Clay__SIMDARXMix(uint64x2_t* a, uint64x2_t* b) {
+ *a = vaddq_u64(*a, *b);
+ *b = veorq_u64(vorrq_u64(vshlq_n_u64(*b, 17), vshrq_n_u64(*b, 64 - 17)), *a);
+}
+
+uint64_t Clay__HashData(const uint8_t* data, size_t length) {
+ // Pinched these constants from the BLAKE implementation
+ uint64x2_t v0 = vdupq_n_u64(0x6a09e667f3bcc908ULL);
+ uint64x2_t v1 = vdupq_n_u64(0xbb67ae8584caa73bULL);
+ uint64x2_t v2 = vdupq_n_u64(0x3c6ef372fe94f82bULL);
+ uint64x2_t v3 = vdupq_n_u64(0xa54ff53a5f1d36f1ULL);
+
+ uint8_t overflowBuffer[8] = { 0 };
+
+ while (length > 0) {
+ uint64x2_t msg;
+ if (length > 16) {
+ msg = vld1q_u64((const uint64_t*)data);
+ data += 16;
+ length -= 16;
+ }
+ else if (length > 8) {
+ msg = vcombine_u64(vld1_u64((const uint64_t*)data), vdup_n_u64(0));
+ data += 8;
+ length -= 8;
+ }
+ else {
+ for (size_t i = 0; i < length; i++) {
+ overflowBuffer[i] = data[i];
+ }
+ uint8x8_t lower = vld1_u8(overflowBuffer);
+ msg = vreinterpretq_u64_u8(vcombine_u8(lower, vdup_n_u8(0)));
+ length = 0;
+ }
+ v0 = veorq_u64(v0, msg);
+ Clay__SIMDARXMix(&v0, &v1);
+ Clay__SIMDARXMix(&v2, &v3);
+
+ v0 = vaddq_u64(v0, v2);
+ v1 = vaddq_u64(v1, v3);
+ }
+
+ Clay__SIMDARXMix(&v0, &v1);
+ Clay__SIMDARXMix(&v2, &v3);
+ v0 = vaddq_u64(v0, v2);
+ v1 = vaddq_u64(v1, v3);
+ v0 = vaddq_u64(v0, v1);
+
+ uint64_t result[2];
+ vst1q_u64(result, v0);
+
+ return result[0] ^ result[1];
+}
+#else
+uint64_t Clay__HashData(const uint8_t* data, size_t length) {
+ uint64_t hash = 0;
+
+ for (size_t i = 0; i < length; i++) {
+ hash += data[i];
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+ }
+ return hash;
+}
+#endif
+
+uint32_t Clay__HashStringContentsWithConfig(Clay_String *text, Clay_TextElementConfig *config) {
+ uint32_t hash = 0;
+ if (text->isStaticallyAllocated) {
+ hash += (uintptr_t)text->chars;
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+ hash += text->length;
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+ } else {
+ hash = Clay__HashData((const uint8_t *)text->chars, text->length) % UINT32_MAX;
+ }
+
+ hash += config->fontId;
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+
+ hash += config->fontSize;
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+
+ hash += config->letterSpacing;
+ hash += (hash << 10);
+ hash ^= (hash >> 6);
+
+ hash += (hash << 3);
+ hash ^= (hash >> 11);
+ hash += (hash << 15);
+ return hash + 1; // Reserve the hash result of zero as "null id"
+}
+
+Clay__MeasuredWord *Clay__AddMeasuredWord(Clay__MeasuredWord word, Clay__MeasuredWord *previousWord) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->measuredWordsFreeList.length > 0) {
+ uint32_t newItemIndex = Clay__int32_tArray_GetValue(&context->measuredWordsFreeList, (int)context->measuredWordsFreeList.length - 1);
+ context->measuredWordsFreeList.length--;
+ Clay__MeasuredWordArray_Set(&context->measuredWords, (int)newItemIndex, word);
+ previousWord->next = (int32_t)newItemIndex;
+ return Clay__MeasuredWordArray_Get(&context->measuredWords, (int)newItemIndex);
+ } else {
+ previousWord->next = (int32_t)context->measuredWords.length;
+ return Clay__MeasuredWordArray_Add(&context->measuredWords, word);
+ }
+}
+
+Clay__MeasureTextCacheItem *Clay__MeasureTextCached(Clay_String *text, Clay_TextElementConfig *config) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ #ifndef CLAY_WASM
+ if (!Clay__MeasureText) {
+ if (!context->booleanWarnings.textMeasurementFunctionNotSet) {
+ context->booleanWarnings.textMeasurementFunctionNotSet = true;
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED,
+ .errorText = CLAY_STRING("Clay's internal MeasureText function is null. You may have forgotten to call Clay_SetMeasureTextFunction(), or passed a NULL function pointer by mistake."),
+ .userData = context->errorHandler.userData });
+ }
+ return &Clay__MeasureTextCacheItem_DEFAULT;
+ }
+ #endif
+ uint32_t id = Clay__HashStringContentsWithConfig(text, config);
+ uint32_t hashBucket = id % (context->maxMeasureTextCacheWordCount / 32);
+ int32_t elementIndexPrevious = 0;
+ int32_t elementIndex = context->measureTextHashMap.internalArray[hashBucket];
+ while (elementIndex != 0) {
+ Clay__MeasureTextCacheItem *hashEntry = Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, elementIndex);
+ if (hashEntry->id == id) {
+ hashEntry->generation = context->generation;
+ return hashEntry;
+ }
+ // This element hasn't been seen in a few frames, delete the hash map item
+ if (context->generation - hashEntry->generation > 2) {
+ // Add all the measured words that were included in this measurement to the freelist
+ int32_t nextWordIndex = hashEntry->measuredWordsStartIndex;
+ while (nextWordIndex != -1) {
+ Clay__MeasuredWord *measuredWord = Clay__MeasuredWordArray_Get(&context->measuredWords, nextWordIndex);
+ Clay__int32_tArray_Add(&context->measuredWordsFreeList, nextWordIndex);
+ nextWordIndex = measuredWord->next;
+ }
+
+ int32_t nextIndex = hashEntry->nextIndex;
+ Clay__MeasureTextCacheItemArray_Set(&context->measureTextHashMapInternal, elementIndex, CLAY__INIT(Clay__MeasureTextCacheItem) { .measuredWordsStartIndex = -1 });
+ Clay__int32_tArray_Add(&context->measureTextHashMapInternalFreeList, elementIndex);
+ if (elementIndexPrevious == 0) {
+ context->measureTextHashMap.internalArray[hashBucket] = nextIndex;
+ } else {
+ Clay__MeasureTextCacheItem *previousHashEntry = Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, elementIndexPrevious);
+ previousHashEntry->nextIndex = nextIndex;
+ }
+ elementIndex = nextIndex;
+ } else {
+ elementIndexPrevious = elementIndex;
+ elementIndex = hashEntry->nextIndex;
+ }
+ }
+
+ int32_t newItemIndex = 0;
+ Clay__MeasureTextCacheItem newCacheItem = { .measuredWordsStartIndex = -1, .id = id, .generation = context->generation };
+ Clay__MeasureTextCacheItem *measured = NULL;
+ if (context->measureTextHashMapInternalFreeList.length > 0) {
+ newItemIndex = Clay__int32_tArray_GetValue(&context->measureTextHashMapInternalFreeList, context->measureTextHashMapInternalFreeList.length - 1);
+ context->measureTextHashMapInternalFreeList.length--;
+ Clay__MeasureTextCacheItemArray_Set(&context->measureTextHashMapInternal, newItemIndex, newCacheItem);
+ measured = Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, newItemIndex);
+ } else {
+ if (context->measureTextHashMapInternal.length == context->measureTextHashMapInternal.capacity - 1) {
+ if (!context->booleanWarnings.maxTextMeasureCacheExceeded) {
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED,
+ .errorText = CLAY_STRING("Clay ran out of capacity while attempting to measure text elements. Try using Clay_SetMaxElementCount() with a higher value."),
+ .userData = context->errorHandler.userData });
+ context->booleanWarnings.maxTextMeasureCacheExceeded = true;
+ }
+ return &Clay__MeasureTextCacheItem_DEFAULT;
+ }
+ measured = Clay__MeasureTextCacheItemArray_Add(&context->measureTextHashMapInternal, newCacheItem);
+ newItemIndex = context->measureTextHashMapInternal.length - 1;
+ }
+
+ int32_t start = 0;
+ int32_t end = 0;
+ float lineWidth = 0;
+ float measuredWidth = 0;
+ float measuredHeight = 0;
+ float spaceWidth = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = 1, .chars = CLAY__SPACECHAR.chars, .baseChars = CLAY__SPACECHAR.chars }, config, context->measureTextUserData).width;
+ Clay__MeasuredWord tempWord = { .next = -1 };
+ Clay__MeasuredWord *previousWord = &tempWord;
+ while (end < text->length) {
+ if (context->measuredWords.length == context->measuredWords.capacity - 1) {
+ if (!context->booleanWarnings.maxTextMeasureCacheExceeded) {
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_TEXT_MEASUREMENT_CAPACITY_EXCEEDED,
+ .errorText = CLAY_STRING("Clay has run out of space in it's internal text measurement cache. Try using Clay_SetMaxMeasureTextCacheWordCount() (default 16384, with 1 unit storing 1 measured word)."),
+ .userData = context->errorHandler.userData });
+ context->booleanWarnings.maxTextMeasureCacheExceeded = true;
+ }
+ return &Clay__MeasureTextCacheItem_DEFAULT;
+ }
+ char current = text->chars[end];
+ if (current == ' ' || current == '\n') {
+ int32_t length = end - start;
+ Clay_Dimensions dimensions = CLAY__DEFAULT_STRUCT;
+ if (length > 0) {
+ dimensions = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) {.length = length, .chars = &text->chars[start], .baseChars = text->chars}, config, context->measureTextUserData);
+ }
+ measured->minWidth = CLAY__MAX(dimensions.width, measured->minWidth);
+ measuredHeight = CLAY__MAX(measuredHeight, dimensions.height);
+ if (current == ' ') {
+ dimensions.width += spaceWidth;
+ previousWord = Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = length + 1, .width = dimensions.width, .next = -1 }, previousWord);
+ lineWidth += dimensions.width;
+ }
+ if (current == '\n') {
+ if (length > 0) {
+ previousWord = Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = length, .width = dimensions.width, .next = -1 }, previousWord);
+ }
+ previousWord = Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = end + 1, .length = 0, .width = 0, .next = -1 }, previousWord);
+ lineWidth += dimensions.width;
+ measuredWidth = CLAY__MAX(lineWidth, measuredWidth);
+ measured->containsNewlines = true;
+ lineWidth = 0;
+ }
+ start = end + 1;
+ }
+ end++;
+ }
+ if (end - start > 0) {
+ Clay_Dimensions dimensions = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = end - start, .chars = &text->chars[start], .baseChars = text->chars }, config, context->measureTextUserData);
+ Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = end - start, .width = dimensions.width, .next = -1 }, previousWord);
+ lineWidth += dimensions.width;
+ measuredHeight = CLAY__MAX(measuredHeight, dimensions.height);
+ measured->minWidth = CLAY__MAX(dimensions.width, measured->minWidth);
+ }
+ measuredWidth = CLAY__MAX(lineWidth, measuredWidth) - config->letterSpacing;
+
+ measured->measuredWordsStartIndex = tempWord.next;
+ measured->unwrappedDimensions.width = measuredWidth;
+ measured->unwrappedDimensions.height = measuredHeight;
+
+ if (elementIndexPrevious != 0) {
+ Clay__MeasureTextCacheItemArray_Get(&context->measureTextHashMapInternal, elementIndexPrevious)->nextIndex = newItemIndex;
+ } else {
+ context->measureTextHashMap.internalArray[hashBucket] = newItemIndex;
+ }
+ return measured;
+}
+
+bool Clay__PointIsInsideRect(Clay_Vector2 point, Clay_BoundingBox rect) {
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
+}
+
+Clay_LayoutElementHashMapItem* Clay__AddHashMapItem(Clay_ElementId elementId, Clay_LayoutElement* layoutElement) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->layoutElementsHashMapInternal.length == context->layoutElementsHashMapInternal.capacity - 1) {
+ if (!context->booleanWarnings.hashMapCapacityExceeded) {
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_HASH_MAP_CAPACITY_EXCEEDED,
+ .errorText = CLAY_STRING("Clay has run out of space in it's internal element ID hashmap. Try using Clay_SetMaxElementCount() with a higher value."),
+ .userData = context->errorHandler.userData });
+ context->booleanWarnings.hashMapCapacityExceeded = true;
+ }
+ return NULL;
+ }
+ Clay_LayoutElementHashMapItem item = { .elementId = elementId, .layoutElement = layoutElement, .nextIndex = -1, .generation = context->generation + 1, .appearedThisFrame = true };
+ uint32_t hashBucket = elementId.id % context->layoutElementsHashMap.capacity;
+ int32_t hashItemPrevious = -1;
+ int32_t hashItemIndex = context->layoutElementsHashMap.internalArray[hashBucket];
+ while (hashItemIndex != -1) { // Just replace collision, not a big deal - leave it up to the end user
+ Clay_LayoutElementHashMapItem *hashItem = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, hashItemIndex);
+ if (hashItem->elementId.id == elementId.id) { // Collision - resolve based on generation
+ item.nextIndex = hashItem->nextIndex;
+ if (hashItem->generation <= context->generation) { // First collision - assume this is the "same" element
+ hashItem->appearedThisFrame = hashItem->generation < context->generation;
+ hashItem->elementId = elementId; // Make sure to copy this across. If the stringId reference has changed, we should update the hash item to use the new one.
+ hashItem->generation = context->generation + 1;
+ hashItem->layoutElement = layoutElement;
+ hashItem->debugData.collision = false;
+ hashItem->onHoverFunction = NULL;
+ hashItem->hoverFunctionUserData = 0;
+ } else { // Multiple collisions this frame - two elements have the same ID
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_DUPLICATE_ID,
+ .errorText = CLAY_STRING("An element with this ID was already previously declared during this layout."),
+ .userData = context->errorHandler.userData });
+ if (context->debugModeEnabled) {
+ hashItem->debugData.collision = true;
+ }
+ }
+ return hashItem;
+ }
+ hashItemPrevious = hashItemIndex;
+ hashItemIndex = hashItem->nextIndex;
+ }
+
+ int32_t indexToUse = 0;
+ if (context->layoutElementsHashMapFreeList.length > 0) {
+ indexToUse = Clay__int32_tArray_GetValue(&context->layoutElementsHashMapFreeList, context->layoutElementsHashMapFreeList.length - 1);
+ context->layoutElementsHashMapFreeList.length--;
+ } else {
+ indexToUse = context->layoutElementsHashMapInternal.length;
+ }
+ Clay_LayoutElementHashMapItem *hashItem = Clay__LayoutElementHashMapItemArray_Set(&context->layoutElementsHashMapInternal, indexToUse, item);
+ if (hashItemPrevious != -1) {
+ Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, hashItemPrevious)->nextIndex = (int32_t)indexToUse;
+ } else {
+ context->layoutElementsHashMap.internalArray[hashBucket] = (int32_t)indexToUse;
+ }
+ return hashItem;
+}
+
+Clay_LayoutElementHashMapItem *Clay__GetHashMapItem(uint32_t id) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ uint32_t hashBucket = id % context->layoutElementsHashMap.capacity;
+ int32_t elementIndex = context->layoutElementsHashMap.internalArray[hashBucket];
+ while (elementIndex != -1) {
+ Clay_LayoutElementHashMapItem *hashEntry = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, elementIndex);
+ if (hashEntry->elementId.id == id) {
+ return hashEntry;
+ }
+ elementIndex = hashEntry->nextIndex;
+ }
+ return &Clay_LayoutElementHashMapItem_DEFAULT;
+}
+
+void Clay__UpdateAspectRatioBox(Clay_LayoutElement *layoutElement) {
+ if (layoutElement->config.aspectRatio.aspectRatio != 0) {
+ if (layoutElement->dimensions.width == 0 && layoutElement->dimensions.height != 0) {
+ layoutElement->dimensions.width = layoutElement->dimensions.height * layoutElement->config.aspectRatio.aspectRatio;
+ } else if (layoutElement->dimensions.width != 0 && layoutElement->dimensions.height == 0) {
+ layoutElement->dimensions.height = layoutElement->dimensions.width * (1 / layoutElement->config.aspectRatio.aspectRatio);
+ }
+ }
+}
+
+void Clay__CloseElement(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->booleanWarnings.maxElementsExceeded) {
+ return;
+ }
+ Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+ Clay_LayoutConfig *layoutConfig = &openLayoutElement->config.layout;
+ bool elementHasClipHorizontal = openLayoutElement->config.clip.horizontal;
+ bool elementHasClipVertical = openLayoutElement->config.clip.vertical;
+ if (elementHasClipHorizontal || elementHasClipVertical || openLayoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+ context->openClipElementStack.length--;
+ }
+
+ float leftRightPadding = (float)(layoutConfig->padding.left + layoutConfig->padding.right);
+ float topBottomPadding = (float)(layoutConfig->padding.top + layoutConfig->padding.bottom);
+
+ // Attach children to the current open element
+ openLayoutElement->children.elements = &context->layoutElementChildren.internalArray[context->layoutElementChildren.length];
+ if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+ openLayoutElement->dimensions.width = leftRightPadding;
+ openLayoutElement->minDimensions.width = leftRightPadding;
+ for (int32_t i = 0; i < openLayoutElement->children.length; i++) {
+ int32_t childIndex = Clay__int32_tArray_GetValue(&context->layoutElementChildrenBuffer, (int)context->layoutElementChildrenBuffer.length - openLayoutElement->children.length + i);
+ Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, childIndex);
+ openLayoutElement->dimensions.width += child->dimensions.width;
+ openLayoutElement->dimensions.height = CLAY__MAX(openLayoutElement->dimensions.height, child->dimensions.height + topBottomPadding);
+ // Minimum size of child elements doesn't matter to clip containers as they can shrink and hide their contents
+ if (!elementHasClipHorizontal) {
+ openLayoutElement->minDimensions.width += child->minDimensions.width;
+ }
+ if (!elementHasClipVertical) {
+ openLayoutElement->minDimensions.height = CLAY__MAX(openLayoutElement->minDimensions.height, child->minDimensions.height + topBottomPadding);
+ }
+ Clay__int32_tArray_Add(&context->layoutElementChildren, childIndex);
+ }
+ float childGap = (float)(CLAY__MAX(openLayoutElement->children.length - 1, 0) * layoutConfig->childGap);
+ openLayoutElement->dimensions.width += childGap;
+ if (!elementHasClipHorizontal) {
+ openLayoutElement->minDimensions.width += childGap;
+ }
+ }
+ else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) {
+ openLayoutElement->dimensions.height = topBottomPadding;
+ openLayoutElement->minDimensions.height = topBottomPadding;
+ for (int32_t i = 0; i < openLayoutElement->children.length; i++) {
+ int32_t childIndex = Clay__int32_tArray_GetValue(&context->layoutElementChildrenBuffer, (int)context->layoutElementChildrenBuffer.length - openLayoutElement->children.length + i);
+ Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, childIndex);
+ openLayoutElement->dimensions.height += child->dimensions.height;
+ openLayoutElement->dimensions.width = CLAY__MAX(openLayoutElement->dimensions.width, child->dimensions.width + leftRightPadding);
+ // Minimum size of child elements doesn't matter to clip containers as they can shrink and hide their contents
+ if (!elementHasClipVertical) {
+ openLayoutElement->minDimensions.height += child->minDimensions.height;
+ }
+ if (!elementHasClipHorizontal) {
+ openLayoutElement->minDimensions.width = CLAY__MAX(openLayoutElement->minDimensions.width, child->minDimensions.width + leftRightPadding);
+ }
+ Clay__int32_tArray_Add(&context->layoutElementChildren, childIndex);
+ }
+ float childGap = (float)(CLAY__MAX(openLayoutElement->children.length - 1, 0) * layoutConfig->childGap);
+ openLayoutElement->dimensions.height += childGap;
+ if (!elementHasClipVertical) {
+ openLayoutElement->minDimensions.height += childGap;
+ }
+ }
+
+ context->layoutElementChildrenBuffer.length -= openLayoutElement->children.length;
+
+ // Clamp element min and max width to the values configured in the layout
+ if (layoutConfig->sizing.width.type != CLAY__SIZING_TYPE_PERCENT) {
+ if (layoutConfig->sizing.width.size.minMax.max <= 0) { // Set the max size if the user didn't specify, makes calculations easier
+ layoutConfig->sizing.width.size.minMax.max = CLAY__MAXFLOAT;
+ }
+ openLayoutElement->dimensions.width = CLAY__MIN(CLAY__MAX(openLayoutElement->dimensions.width, layoutConfig->sizing.width.size.minMax.min), layoutConfig->sizing.width.size.minMax.max);
+ openLayoutElement->minDimensions.width = CLAY__MIN(CLAY__MAX(openLayoutElement->minDimensions.width, layoutConfig->sizing.width.size.minMax.min), layoutConfig->sizing.width.size.minMax.max);
+ } else {
+ openLayoutElement->dimensions.width = 0;
+ }
+
+ // Clamp element min and max height to the values configured in the layout
+ if (layoutConfig->sizing.height.type != CLAY__SIZING_TYPE_PERCENT) {
+ if (layoutConfig->sizing.height.size.minMax.max <= 0) { // Set the max size if the user didn't specify, makes calculations easier
+ layoutConfig->sizing.height.size.minMax.max = CLAY__MAXFLOAT;
+ }
+ openLayoutElement->dimensions.height = CLAY__MIN(CLAY__MAX(openLayoutElement->dimensions.height, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max);
+ openLayoutElement->minDimensions.height = CLAY__MIN(CLAY__MAX(openLayoutElement->minDimensions.height, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max);
+ } else {
+ openLayoutElement->dimensions.height = 0;
+ }
+
+ Clay__UpdateAspectRatioBox(openLayoutElement);
+
+ bool elementIsFloating = openLayoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE;
+
+ // Close the currently open element
+ int32_t closingElementIndex = Clay__int32_tArray_RemoveSwapback(&context->openLayoutElementStack, (int)context->openLayoutElementStack.length - 1);
+
+ // Get the currently open parent
+ openLayoutElement = Clay__GetOpenLayoutElement();
+
+ if (context->openLayoutElementStack.length > 1) {
+ if(elementIsFloating) {
+ openLayoutElement->floatingChildrenCount++;
+ return;
+ }
+ openLayoutElement->children.length++;
+ Clay__int32_tArray_Add(&context->layoutElementChildrenBuffer, closingElementIndex);
+ }
+}
+
+bool Clay__MemCmp(const char *s1, const char *s2, int32_t length);
+#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
+ bool Clay__MemCmp(const char *s1, const char *s2, int32_t length) {
+ while (length >= 16) {
+ __m128i v1 = _mm_loadu_si128((const __m128i *)s1);
+ __m128i v2 = _mm_loadu_si128((const __m128i *)s2);
+
+ if (_mm_movemask_epi8(_mm_cmpeq_epi8(v1, v2)) != 0xFFFF) { // If any byte differs
+ return false;
+ }
+
+ s1 += 16;
+ s2 += 16;
+ length -= 16;
+ }
+
+ // Handle remaining bytes
+ while (length--) {
+ if (*s1 != *s2) {
+ return false;
+ }
+ s1++;
+ s2++;
+ }
+
+ return true;
+ }
+#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__)
+ bool Clay__MemCmp(const char *s1, const char *s2, int32_t length) {
+ while (length >= 16) {
+ uint8x16_t v1 = vld1q_u8((const uint8_t *)s1);
+ uint8x16_t v2 = vld1q_u8((const uint8_t *)s2);
+
+ // Compare vectors
+ if (vminvq_u32(vreinterpretq_u32_u8(vceqq_u8(v1, v2))) != 0xFFFFFFFF) { // If there's a difference
+ return false;
+ }
+
+ s1 += 16;
+ s2 += 16;
+ length -= 16;
+ }
+
+ // Handle remaining bytes
+ while (length--) {
+ if (*s1 != *s2) {
+ return false;
+ }
+ s1++;
+ s2++;
+ }
+
+ return true;
+ }
+#else
+ bool Clay__MemCmp(const char *s1, const char *s2, int32_t length) {
+ for (int32_t i = 0; i < length; i++) {
+ if (s1[i] != s2[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+#endif
+
+void Clay__OpenElement(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) {
+ context->booleanWarnings.maxElementsExceeded = true;
+ return;
+ }
+ Clay_LayoutElement layoutElement = CLAY__DEFAULT_STRUCT;
+ Clay_LayoutElement* openLayoutElement = Clay_LayoutElementArray_Add(&context->layoutElements, layoutElement);
+ Clay__int32_tArray_Add(&context->openLayoutElementStack, context->layoutElements.length - 1);
+ // Generate an ID
+ Clay_LayoutElement *parentElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2));
+ uint32_t offset = parentElement->children.length + parentElement->floatingChildrenCount;
+ Clay_ElementId elementId = Clay__HashNumber(offset, parentElement->id);
+ openLayoutElement->id = elementId.id;
+ Clay__AddHashMapItem(elementId, openLayoutElement);
+ Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId);
+ if (context->openClipElementStack.length > 0) {
+ Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1));
+ } else {
+ Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, 0);
+ }
+}
+
+void Clay__OpenElementWithId(Clay_ElementId elementId) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) {
+ context->booleanWarnings.maxElementsExceeded = true;
+ return;
+ }
+ Clay_LayoutElement layoutElement = CLAY__DEFAULT_STRUCT;
+ layoutElement.id = elementId.id;
+ Clay_LayoutElement * openLayoutElement = Clay_LayoutElementArray_Add(&context->layoutElements, layoutElement);
+ Clay__int32_tArray_Add(&context->openLayoutElementStack, context->layoutElements.length - 1);
+ Clay__AddHashMapItem(elementId, openLayoutElement);
+ Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId);
+ if (context->openClipElementStack.length > 0) {
+ Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1));
+ } else {
+ Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, 0);
+ }
+}
+
+void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig textConfig) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) {
+ context->booleanWarnings.maxElementsExceeded = true;
+ return;
+ }
+ Clay_LayoutElement *parentElement = Clay__GetOpenLayoutElement();
+
+ Clay_LayoutElement layoutElement = { .textConfig = textConfig, .isTextElement = true };
+ Clay_LayoutElement *textElement = Clay_LayoutElementArray_Add(&context->layoutElements, layoutElement);
+ if (context->openClipElementStack.length > 0) {
+ Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1));
+ } else {
+ Clay__int32_tArray_Set(&context->layoutElementClipElementIds, context->layoutElements.length - 1, 0);
+ }
+
+ Clay__int32_tArray_Add(&context->layoutElementChildrenBuffer, context->layoutElements.length - 1);
+ Clay__MeasureTextCacheItem *textMeasured = Clay__MeasureTextCached(&text, &textConfig);
+ Clay_ElementId elementId = Clay__HashNumber(parentElement->children.length + parentElement->floatingChildrenCount, parentElement->id);
+ textElement->id = elementId.id;
+ Clay__AddHashMapItem(elementId, textElement);
+ Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId);
+ Clay_Dimensions textDimensions = { .width = textMeasured->unwrappedDimensions.width, .height = textConfig.lineHeight > 0 ? (float)textConfig.lineHeight : textMeasured->unwrappedDimensions.height };
+ textElement->dimensions = textDimensions;
+ textElement->minDimensions = CLAY__INIT(Clay_Dimensions) { .width = textMeasured->minWidth, .height = textDimensions.height };
+ textElement->textElementData = CLAY__INIT(Clay__TextElementData) { .text = text, .preferredDimensions = textMeasured->unwrappedDimensions };
+ parentElement->children.length++;
+}
+
+void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *declaration) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+ openLayoutElement->config = *declaration;
+ if ((declaration->layout.sizing.width.type == CLAY__SIZING_TYPE_PERCENT && declaration->layout.sizing.width.size.percent > 1) || (declaration->layout.sizing.height.type == CLAY__SIZING_TYPE_PERCENT && declaration->layout.sizing.height.size.percent > 1)) {
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_PERCENTAGE_OVER_1,
+ .errorText = CLAY_STRING("An element was configured with CLAY_SIZING_PERCENT, but the provided percentage value was over 1.0. Clay expects a value between 0 and 1, i.e. 20% is 0.2."),
+ .userData = context->errorHandler.userData });
+ }
+
+ if (declaration->floating.attachTo != CLAY_ATTACH_TO_NONE) {
+ Clay_FloatingElementConfig* floatingConfig = &openLayoutElement->config.floating;
+ // This looks dodgy but because of the auto generated root element the depth of the tree will always be at least 2 here
+ Clay_LayoutElement *hierarchicalParent = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2));
+ if (hierarchicalParent) {
+ uint32_t clipElementId = 0;
+ if (declaration->floating.attachTo == CLAY_ATTACH_TO_PARENT) {
+ // Attach to the element's direct hierarchical parent
+ floatingConfig->parentId = hierarchicalParent->id;
+ if (context->openClipElementStack.length > 0) {
+ clipElementId = Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1);
+ }
+ } else if (declaration->floating.attachTo == CLAY_ATTACH_TO_ELEMENT_WITH_ID) {
+ Clay_LayoutElementHashMapItem *parentItem = Clay__GetHashMapItem(floatingConfig->parentId);
+ if (parentItem == &Clay_LayoutElementHashMapItem_DEFAULT) {
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_FLOATING_CONTAINER_PARENT_NOT_FOUND,
+ .errorText = CLAY_STRING("A floating element was declared with a parentId, but no element with that ID was found."),
+ .userData = context->errorHandler.userData });
+ } else {
+ clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, (int32_t)(parentItem->layoutElement - context->layoutElements.internalArray));
+ }
+ } else if (declaration->floating.attachTo == CLAY_ATTACH_TO_ROOT) {
+ floatingConfig->parentId = Clay__HashString(CLAY_STRING("Clay__RootContainer"), 0).id;
+ }
+ if (declaration->floating.clipTo == CLAY_CLIP_TO_NONE) {
+ clipElementId = 0;
+ }
+ int32_t currentElementIndex = Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1);
+ Clay__int32_tArray_Set(&context->layoutElementClipElementIds, currentElementIndex, clipElementId);
+ Clay__int32_tArray_Add(&context->openClipElementStack, clipElementId);
+ Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) {
+ .layoutElementIndex = Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1),
+ .parentId = floatingConfig->parentId,
+ .clipElementId = clipElementId,
+ .zIndex = floatingConfig->zIndex,
+ });
+ }
+ }
+
+ if (declaration->clip.horizontal || declaration->clip.vertical) {
+ Clay__int32_tArray_Add(&context->openClipElementStack, (int)openLayoutElement->id);
+ // Retrieve or create cached data to track scroll position across frames
+ Clay__ScrollContainerDataInternal *scrollOffset = CLAY__NULL;
+ for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+ Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+ if (openLayoutElement->id == mapping->elementId) {
+ scrollOffset = mapping;
+ scrollOffset->layoutElement = openLayoutElement;
+ scrollOffset->openThisFrame = true;
+ }
+ }
+ if (!scrollOffset) {
+ scrollOffset = Clay__ScrollContainerDataInternalArray_Add(&context->scrollContainerDatas, CLAY__INIT(Clay__ScrollContainerDataInternal){.layoutElement = openLayoutElement, .scrollOrigin = {-1,-1}, .elementId = openLayoutElement->id, .openThisFrame = true});
+ }
+ if (context->externalScrollHandlingEnabled) {
+ scrollOffset->scrollPosition = Clay__QueryScrollOffset(scrollOffset->elementId, context->queryScrollOffsetUserData);
+ }
+ }
+ // Setup data to track transitions across frames
+ if (declaration->transition.handler) {
+ Clay__TransitionDataInternal *transitionData = CLAY__NULL;
+ Clay_LayoutElement* parentElement = Clay__GetParentElement();
+ for (int32_t i = 0; i < context->transitionDatas.length; i++) {
+ Clay__TransitionDataInternal *existingData = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+ if (openLayoutElement->id == existingData->elementId) {
+ if (existingData->state == CLAY_TRANSITION_STATE_EXITING) {
+ existingData->state = CLAY_TRANSITION_STATE_IDLE;
+ Clay_LayoutElementHashMapItem* hashMapItem = Clay__GetHashMapItem(openLayoutElement->id);
+ hashMapItem->appearedThisFrame = false;
+ }
+ transitionData = existingData;
+ transitionData->elementThisFrame = openLayoutElement;
+ if (transitionData->parentId != parentElement->id) {
+ transitionData->reparented = true;
+ }
+ transitionData->parentId = parentElement->id;
+ transitionData->siblingIndex = parentElement->children.length;
+ transitionData->transitionOut = !!declaration->transition.exit.setFinalState;
+ }
+ }
+ if (!transitionData) {
+ transitionData = Clay__TransitionDataInternalArray_Add(&context->transitionDatas, CLAY__INIT(Clay__TransitionDataInternal){
+ .elementThisFrame = openLayoutElement,
+ .elementId = openLayoutElement->id,
+ .parentId = parentElement->id,
+ .siblingIndex = parentElement->children.length,
+ .transitionOut = !!declaration->transition.exit.setFinalState
+ });
+ }
+ }
+}
+
+void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) {
+ Clay__ConfigureOpenElementPtr(&declaration);
+}
+
+void Clay__InitializeEphemeralMemory(Clay_Context* context) {
+ int32_t maxElementCount = context->maxElementCount;
+ // Ephemeral Memory - reset every frame
+ Clay_Arena *arena = &context->internalArena;
+ arena->nextAllocation = context->arenaResetOffset;
+
+ context->layoutElementChildrenBuffer = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->layoutElements = Clay_LayoutElementArray_Allocate_Arena(maxElementCount, arena);
+ context->warnings = Clay__WarningArray_Allocate_Arena(100, arena);
+
+ context->layoutElementIdStrings = Clay__StringArray_Allocate_Arena(maxElementCount, arena);
+ context->wrappedTextLines = Clay__WrappedTextLineArray_Allocate_Arena(maxElementCount, arena);
+ context->layoutElementTreeNodeArray1 = Clay__LayoutElementTreeNodeArray_Allocate_Arena(maxElementCount, arena);
+ context->layoutElementTreeRoots = Clay__LayoutElementTreeRootArray_Allocate_Arena(maxElementCount, arena);
+ context->layoutElementChildren = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->openLayoutElementStack = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->renderCommands = Clay_RenderCommandArray_Allocate_Arena(maxElementCount, arena);
+ context->treeNodeVisited = Clay__boolArray_Allocate_Arena(maxElementCount, arena);
+ context->treeNodeVisited.length = context->treeNodeVisited.capacity; // This array is accessed directly rather than behaving as a list
+ context->openClipElementStack = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->reusableElementIndexBuffer = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->layoutElementClipElementIds = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->dynamicStringData = Clay__charArray_Allocate_Arena(maxElementCount, arena);
+}
+
+void Clay__InitializePersistentMemory(Clay_Context* context) {
+ // Persistent memory - initialized once and not reset
+ int32_t maxElementCount = context->maxElementCount;
+ int32_t maxMeasureTextCacheWordCount = context->maxMeasureTextCacheWordCount;
+ Clay_Arena *arena = &context->internalArena;
+
+ context->scrollContainerDatas = Clay__ScrollContainerDataInternalArray_Allocate_Arena(100, arena);
+ context->transitionDatas = Clay__TransitionDataInternalArray_Allocate_Arena(200, arena);
+ context->layoutElementsHashMapInternal = Clay__LayoutElementHashMapItemArray_Allocate_Arena(maxElementCount, arena);
+ context->layoutElementsHashMap = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->layoutElementsHashMapFreeList = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->measureTextHashMapInternal = Clay__MeasureTextCacheItemArray_Allocate_Arena(maxElementCount, arena);
+ context->measureTextHashMapInternalFreeList = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->measuredWordsFreeList = Clay__int32_tArray_Allocate_Arena(maxMeasureTextCacheWordCount, arena);
+ context->measureTextHashMap = Clay__int32_tArray_Allocate_Arena(maxElementCount, arena);
+ context->measuredWords = Clay__MeasuredWordArray_Allocate_Arena(maxMeasureTextCacheWordCount, arena);
+ context->pointerOverIds = Clay_ElementIdArray_Allocate_Arena(maxElementCount, arena);
+ context->arenaResetOffset = arena->nextAllocation;
+}
+
+const float CLAY__EPSILON = 0.01;
+
+bool Clay__FloatEqual(float left, float right) {
+ float subtracted = left - right;
+ return subtracted < CLAY__EPSILON && subtracted > -CLAY__EPSILON;
+}
+
+Clay_SizingAxis Clay__GetElementSizing(Clay_LayoutElement* element, bool xAxis) {
+ if (element->isTextElement) {
+ return CLAY__INIT(Clay_SizingAxis) {};
+ } else {
+ return xAxis ? element->config.layout.sizing.width : element->config.layout.sizing.height;
+ }
+}
+
+// Writes out the location of text elements to layout elements buffer 1
+void Clay__SizeContainersAlongAxis(bool xAxis, float deltaTime, Clay__int32_tArray* textElementsOut, Clay__int32_tArray* aspectRatioElementsOut) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ Clay__int32_tArray bfsBuffer = context->layoutElementChildrenBuffer;
+ Clay__int32_tArray resizableContainerBuffer = context->openLayoutElementStack;
+ for (int32_t rootIndex = 0; rootIndex < context->layoutElementTreeRoots.length; ++rootIndex) {
+ bfsBuffer.length = 0;
+ Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex);
+ Clay_LayoutElement *rootElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)root->layoutElementIndex);
+ Clay__int32_tArray_Add(&bfsBuffer, (int32_t)root->layoutElementIndex);
+
+ // Size floating containers to their parents
+ if (rootElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+ Clay_FloatingElementConfig *floatingElementConfig = &rootElement->config.floating;
+ Clay_LayoutElementHashMapItem *parentItem = Clay__GetHashMapItem(floatingElementConfig->parentId);
+ if (parentItem && parentItem != &Clay_LayoutElementHashMapItem_DEFAULT) {
+ Clay_LayoutElement *parentLayoutElement = parentItem->layoutElement;
+ switch (rootElement->config.layout.sizing.width.type) {
+ case CLAY__SIZING_TYPE_GROW: {
+ rootElement->dimensions.width = parentLayoutElement->dimensions.width;
+ break;
+ }
+ case CLAY__SIZING_TYPE_PERCENT: {
+ rootElement->dimensions.width = parentLayoutElement->dimensions.width * rootElement->config.layout.sizing.width.size.percent;
+ break;
+ }
+ default: break;
+ }
+ switch (rootElement->config.layout.sizing.height.type) {
+ case CLAY__SIZING_TYPE_GROW: {
+ rootElement->dimensions.height = parentLayoutElement->dimensions.height;
+ break;
+ }
+ case CLAY__SIZING_TYPE_PERCENT: {
+ rootElement->dimensions.height = parentLayoutElement->dimensions.height * rootElement->config.layout.sizing.height.size.percent;
+ break;
+ }
+ default: break;
+ }
+ }
+ }
+
+ if (rootElement->config.layout.sizing.width.type != CLAY__SIZING_TYPE_PERCENT) {
+ rootElement->dimensions.width = CLAY__MIN(CLAY__MAX(rootElement->dimensions.width, rootElement->config.layout.sizing.width.size.minMax.min), rootElement->config.layout.sizing.width.size.minMax.max);
+ }
+ if (rootElement->config.layout.sizing.height.type != CLAY__SIZING_TYPE_PERCENT) {
+ rootElement->dimensions.height = CLAY__MIN(CLAY__MAX(rootElement->dimensions.height, rootElement->config.layout.sizing.height.size.minMax.min), rootElement->config.layout.sizing.height.size.minMax.max);
+ }
+
+
+ for (int32_t i = 0; i < bfsBuffer.length; ++i) {
+ int32_t parentIndex = Clay__int32_tArray_GetValue(&bfsBuffer, i);
+ Clay_LayoutElement *parent = Clay_LayoutElementArray_Get(&context->layoutElements, parentIndex);
+ Clay_LayoutConfig *parentLayoutConfig = &parent->config.layout;
+ int32_t growContainerCount = 0;
+ float parentSize = xAxis ? parent->dimensions.width : parent->dimensions.height;
+ float parentPadding = (float)(xAxis ? (parentLayoutConfig->padding.left + parentLayoutConfig->padding.right) : (parentLayoutConfig->padding.top + parentLayoutConfig->padding.bottom));
+ float innerContentSize = 0, totalPaddingAndChildGaps = parentPadding;
+ bool sizingAlongAxis = (xAxis && parentLayoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) || (!xAxis && parentLayoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM);
+ resizableContainerBuffer.length = 0;
+ float parentChildGap = parentLayoutConfig->childGap;
+ bool isFirstChild = true;
+
+ for (int32_t childOffset = 0; childOffset < parent->children.length; childOffset++) {
+ int32_t childElementIndex = parent->children.elements[childOffset];
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, childElementIndex);
+ Clay_SizingAxis childSizing = Clay__GetElementSizing(childElement, xAxis);
+ float childSize = xAxis ? childElement->dimensions.width : childElement->dimensions.height;
+
+ if (textElementsOut && childElement->isTextElement) {
+ Clay__int32_tArray_Add(textElementsOut, childElementIndex);
+ } else if (childElement->children.length > 0) {
+ Clay__int32_tArray_Add(&bfsBuffer, childElementIndex);
+ }
+
+ if (!childElement->isTextElement && aspectRatioElementsOut && childElement->config.aspectRatio.aspectRatio != 0) {
+ Clay__int32_tArray_Add(aspectRatioElementsOut, childElementIndex);
+ }
+
+ // Note: setting isFirstChild = false is skipped here
+ if (childElement->exiting) {
+ continue;
+ }
+
+ if (childSizing.type != CLAY__SIZING_TYPE_PERCENT
+ && childSizing.type != CLAY__SIZING_TYPE_FIXED
+ && (!childElement->isTextElement || childElement->textConfig.wrapMode == CLAY_TEXT_WRAP_WORDS)
+// && (xAxis || !Clay__ElementHasConfig(childElement, CLAY__ELEMENT_CONFIG_TYPE_ASPECT))
+ ) {
+ Clay__int32_tArray_Add(&resizableContainerBuffer, childElementIndex);
+ }
+
+ if (sizingAlongAxis) {
+ innerContentSize += (childSizing.type == CLAY__SIZING_TYPE_PERCENT ? 0 : childSize);
+ if (childSizing.type == CLAY__SIZING_TYPE_GROW) {
+ growContainerCount++;
+ }
+ if (!isFirstChild) {
+ innerContentSize += parentChildGap; // For children after index 0, the childAxisOffset is the gap from the previous child
+ totalPaddingAndChildGaps += parentChildGap;
+ }
+ } else {
+ innerContentSize = CLAY__MAX(childSize, innerContentSize);
+ }
+ isFirstChild = false;
+ }
+
+ // Expand percentage containers to size
+ for (int32_t childOffset = 0; childOffset < parent->children.length; childOffset++) {
+ int32_t childElementIndex = parent->children.elements[childOffset];
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, childElementIndex);
+ Clay_SizingAxis childSizing = Clay__GetElementSizing(childElement, xAxis);
+ float *childSize = xAxis ? &childElement->dimensions.width : &childElement->dimensions.height;
+ if (childSizing.type == CLAY__SIZING_TYPE_PERCENT) {
+ *childSize = (parentSize - totalPaddingAndChildGaps) * childSizing.size.percent;
+ if (sizingAlongAxis) {
+ innerContentSize += *childSize;
+ }
+ Clay__UpdateAspectRatioBox(childElement);
+ }
+ }
+
+ if (sizingAlongAxis) {
+ float sizeToDistribute = parentSize - parentPadding - innerContentSize;
+ // The content is too large, compress the children as much as possible
+ if (sizeToDistribute < 0) {
+ // If the parent clips content in this axis direction, don't compress children, just leave them alone
+ if (((xAxis && parent->config.clip.horizontal) || (!xAxis && parent->config.clip.vertical))) {
+ continue;
+ }
+ // Scrolling containers preferentially compress before others
+ while (sizeToDistribute < -CLAY__EPSILON && resizableContainerBuffer.length > 0) {
+ float largest = 0;
+ float secondLargest = 0;
+ float widthToAdd = sizeToDistribute;
+ for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+ Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+ float childSize = xAxis ? child->dimensions.width : child->dimensions.height;
+ if (Clay__FloatEqual(childSize, largest)) { continue; }
+ if (childSize > largest) {
+ secondLargest = largest;
+ largest = childSize;
+ }
+ if (childSize < largest) {
+ secondLargest = CLAY__MAX(secondLargest, childSize);
+ widthToAdd = secondLargest - largest;
+ }
+ }
+
+ widthToAdd = CLAY__MAX(widthToAdd, sizeToDistribute / resizableContainerBuffer.length);
+
+ for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+ Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+ float *childSize = xAxis ? &child->dimensions.width : &child->dimensions.height;
+ float minSize = xAxis ? child->minDimensions.width : child->minDimensions.height;
+ float previousWidth = *childSize;
+ if (Clay__FloatEqual(*childSize, largest)) {
+ *childSize += widthToAdd;
+ if (*childSize <= minSize) {
+ *childSize = minSize;
+ Clay__int32_tArray_RemoveSwapback(&resizableContainerBuffer, childIndex--);
+ }
+ sizeToDistribute -= (*childSize - previousWidth);
+ }
+ }
+ }
+ // The content is too small, allow SIZING_GROW containers to expand
+ } else if (sizeToDistribute > 0 && growContainerCount > 0) {
+ for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+ Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+ Clay__SizingType childSizing = Clay__GetElementSizing(child, xAxis).type;
+ if (childSizing != CLAY__SIZING_TYPE_GROW) {
+ Clay__int32_tArray_RemoveSwapback(&resizableContainerBuffer, childIndex--);
+ }
+ }
+ while (sizeToDistribute > CLAY__EPSILON && resizableContainerBuffer.length > 0) {
+ float smallest = CLAY__MAXFLOAT;
+ float secondSmallest = CLAY__MAXFLOAT;
+ float widthToAdd = sizeToDistribute;
+ for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+ Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+ float childSize = xAxis ? child->dimensions.width : child->dimensions.height;
+ if (Clay__FloatEqual(childSize, smallest)) { continue; }
+ if (childSize < smallest) {
+ secondSmallest = smallest;
+ smallest = childSize;
+ }
+ if (childSize > smallest) {
+ secondSmallest = CLAY__MIN(secondSmallest, childSize);
+ widthToAdd = secondSmallest - smallest;
+ }
+ }
+
+ widthToAdd = CLAY__MIN(widthToAdd, sizeToDistribute / resizableContainerBuffer.length);
+
+ for (int childIndex = 0; childIndex < resizableContainerBuffer.length; childIndex++) {
+ Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childIndex));
+ float *childSize = xAxis ? &child->dimensions.width : &child->dimensions.height;
+ Clay_SizingAxis childSizing = Clay__GetElementSizing(child, xAxis);
+ float maxSize = childSizing.size.minMax.max;
+ float previousWidth = *childSize;
+ if (Clay__FloatEqual(*childSize, smallest)) {
+ *childSize += widthToAdd;
+ if (*childSize >= maxSize) {
+ *childSize = maxSize;
+ Clay__int32_tArray_RemoveSwapback(&resizableContainerBuffer, childIndex--);
+ }
+ sizeToDistribute -= (*childSize - previousWidth);
+ }
+ }
+ }
+ }
+ // Sizing along the non layout axis ("off axis")
+ } else {
+ for (int32_t childOffset = 0; childOffset < resizableContainerBuffer.length; childOffset++) {
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childOffset));
+ Clay_SizingAxis childSizing = Clay__GetElementSizing(childElement, xAxis);
+ float minSize = xAxis ? childElement->minDimensions.width : childElement->minDimensions.height;
+ float *childSize = xAxis ? &childElement->dimensions.width : &childElement->dimensions.height;
+
+ float maxSize = parentSize - parentPadding;
+ // If we're laying out the children of a scroll panel, grow containers expand to the size of the inner content, not the outer container
+ if (((xAxis && parent->config.clip.horizontal) || (!xAxis && parent->config.clip.vertical))) {
+ maxSize = CLAY__MAX(maxSize, innerContentSize);
+ }
+ if (childSizing.type == CLAY__SIZING_TYPE_GROW) {
+ *childSize = CLAY__MIN(maxSize, childSizing.size.minMax.max);
+ }
+ *childSize = CLAY__MAX(minSize, CLAY__MIN(*childSize, maxSize));
+ }
+ }
+ }
+ }
+}
+
+Clay_String Clay__IntToString(int32_t integer) {
+ if (integer == 0) {
+ return CLAY__INIT(Clay_String) { .length = 1, .chars = "0" };
+ }
+ Clay_Context* context = Clay_GetCurrentContext();
+ char *chars = (char *)(context->dynamicStringData.internalArray + context->dynamicStringData.length);
+ int32_t length = 0;
+ int32_t sign = integer;
+
+ if (integer < 0) {
+ integer = -integer;
+ }
+ while (integer > 0) {
+ chars[length++] = (char)(integer % 10 + '0');
+ integer /= 10;
+ }
+
+ if (sign < 0) {
+ chars[length++] = '-';
+ }
+
+ // Reverse the string to get the correct order
+ for (int32_t j = 0, k = length - 1; j < k; j++, k--) {
+ char temp = chars[j];
+ chars[j] = chars[k];
+ chars[k] = temp;
+ }
+ context->dynamicStringData.length += length;
+ return CLAY__INIT(Clay_String) { .length = length, .chars = chars };
+}
+
+void Clay__AddRenderCommand(Clay_RenderCommand renderCommand) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->renderCommands.length < context->renderCommands.capacity - 1) {
+ Clay_RenderCommandArray_Add(&context->renderCommands, renderCommand);
+ } else {
+ if (!context->booleanWarnings.maxRenderCommandsExceeded) {
+ context->booleanWarnings.maxRenderCommandsExceeded = true;
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_ELEMENTS_CAPACITY_EXCEEDED,
+ .errorText = CLAY_STRING("Clay ran out of capacity while attempting to create render commands. This is usually caused by a large amount of wrapping text elements while close to the max element capacity. Try using Clay_SetMaxElementCount() with a higher value."),
+ .userData = context->errorHandler.userData });
+ }
+ }
+}
+
+bool Clay__ElementIsOffscreen(Clay_BoundingBox *boundingBox) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->disableCulling) {
+ return false;
+ }
+
+ return (boundingBox->x > (float)context->layoutDimensions.width) ||
+ (boundingBox->y > (float)context->layoutDimensions.height) ||
+ (boundingBox->x + boundingBox->width < 0) ||
+ (boundingBox->y + boundingBox->height < 0);
+}
+
+void Clay__CalculateFinalLayout(float deltaTime, bool useStoredBoundingBoxes, bool generateRenderCommands) {
+ Clay_Context* context = Clay_GetCurrentContext();
+
+ // Calculate sizing along the X axis
+ Clay__int32_tArray textElements = context->openClipElementStack;
+ textElements.length = 0;
+ Clay__int32_tArray aspectRatioElements = context->reusableElementIndexBuffer;
+ aspectRatioElements.length = 0;
+ Clay__SizeContainersAlongAxis(true, deltaTime, &textElements, &aspectRatioElements);
+
+ // Wrap text
+ for (int32_t textElementIndex = 0; textElementIndex < textElements.length; ++textElementIndex) {
+ Clay_LayoutElement *element = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&textElements, textElementIndex));
+ Clay__TextElementData *textElementData = &element->textElementData;
+ textElementData->wrappedLines = CLAY__INIT(Clay__WrappedTextLineArraySlice) { .length = 0, .internalArray = &context->wrappedTextLines.internalArray[context->wrappedTextLines.length] };
+ Clay_LayoutElement *containerElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&textElements, textElementIndex));
+ Clay__MeasureTextCacheItem *measureTextCacheItem = Clay__MeasureTextCached(&textElementData->text, &containerElement->textConfig);
+ float lineWidth = 0;
+ float lineHeight = containerElement->textConfig.lineHeight > 0 ? (float)containerElement->textConfig.lineHeight : textElementData->preferredDimensions.height;
+ int32_t lineLengthChars = 0;
+ int32_t lineStartOffset = 0;
+ if (!measureTextCacheItem->containsNewlines && textElementData->preferredDimensions.width <= containerElement->dimensions.width) {
+ Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { containerElement->dimensions, textElementData->text });
+ textElementData->wrappedLines.length++;
+ continue;
+ }
+ float spaceWidth = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = 1, .chars = CLAY__SPACECHAR.chars, .baseChars = CLAY__SPACECHAR.chars }, &containerElement->textConfig, context->measureTextUserData).width;
+ int32_t wordIndex = measureTextCacheItem->measuredWordsStartIndex;
+ while (wordIndex != -1) {
+ if (context->wrappedTextLines.length > context->wrappedTextLines.capacity - 1) {
+ break;
+ }
+ Clay__MeasuredWord *measuredWord = Clay__MeasuredWordArray_Get(&context->measuredWords, wordIndex);
+ // Only word on the line is too large, just render it anyway
+ if (lineLengthChars == 0 && lineWidth + measuredWord->width > containerElement->dimensions.width) {
+ Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { { measuredWord->width, lineHeight }, { .length = measuredWord->length, .chars = &textElementData->text.chars[measuredWord->startOffset] } });
+ textElementData->wrappedLines.length++;
+ wordIndex = measuredWord->next;
+ lineStartOffset = measuredWord->startOffset + measuredWord->length;
+ }
+ // measuredWord->length == 0 means a newline character
+ else if (measuredWord->length == 0 || lineWidth + measuredWord->width > containerElement->dimensions.width) {
+ // Wrapped text lines list has overflowed, just render out the line
+ bool finalCharIsSpace = textElementData->text.chars[CLAY__MAX(lineStartOffset + lineLengthChars - 1, 0)] == ' ';
+ Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { { lineWidth + (finalCharIsSpace ? -spaceWidth : 0), lineHeight }, { .length = lineLengthChars + (finalCharIsSpace ? -1 : 0), .chars = &textElementData->text.chars[lineStartOffset] } });
+ textElementData->wrappedLines.length++;
+ if (lineLengthChars == 0 || measuredWord->length == 0) {
+ wordIndex = measuredWord->next;
+ }
+ lineWidth = 0;
+ lineLengthChars = 0;
+ lineStartOffset = measuredWord->startOffset;
+ } else {
+ lineWidth += measuredWord->width + containerElement->textConfig.letterSpacing;
+ lineLengthChars += measuredWord->length;
+ wordIndex = measuredWord->next;
+ }
+ }
+ if (lineLengthChars > 0) {
+ Clay__WrappedTextLineArray_Add(&context->wrappedTextLines, CLAY__INIT(Clay__WrappedTextLine) { { lineWidth - containerElement->textConfig.letterSpacing, lineHeight }, {.length = lineLengthChars, .chars = &textElementData->text.chars[lineStartOffset] } });
+ textElementData->wrappedLines.length++;
+ }
+ containerElement->dimensions.height = lineHeight * (float)textElementData->wrappedLines.length;
+ }
+
+ // Scale vertical heights according to aspect ratio
+ for (int32_t i = 0; i < aspectRatioElements.length; ++i) {
+ Clay_LayoutElement* aspectElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&aspectRatioElements, i));
+ aspectElement->dimensions.height = (1 / aspectElement->config.aspectRatio.aspectRatio) * aspectElement->dimensions.width;
+ aspectElement->config.layout.sizing.height.size.minMax.max = aspectElement->dimensions.height;
+ }
+
+ // Propagate effect of text wrapping, aspect scaling etc. on height of parents
+ Clay__LayoutElementTreeNodeArray dfsBuffer = context->layoutElementTreeNodeArray1;
+ dfsBuffer.length = 0;
+ for (int32_t i = 0; i < context->layoutElementTreeRoots.length; ++i) {
+ Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, i);
+ context->treeNodeVisited.internalArray[dfsBuffer.length] = false;
+ Clay__LayoutElementTreeNodeArray_Add(&dfsBuffer, CLAY__INIT(Clay__LayoutElementTreeNode) { .layoutElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)root->layoutElementIndex) });
+ }
+ while (dfsBuffer.length > 0) {
+ Clay__LayoutElementTreeNode *currentElementTreeNode = Clay__LayoutElementTreeNodeArray_Get(&dfsBuffer, (int)dfsBuffer.length - 1);
+ Clay_LayoutElement *currentElement = currentElementTreeNode->layoutElement;
+ if (!context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
+ context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
+ // If the element has no children or is the container for a text element, don't bother inspecting it
+ if (currentElement->isTextElement || currentElement->children.length == 0) {
+ dfsBuffer.length--;
+ continue;
+ }
+ // Add the children to the DFS buffer (needs to be pushed in reverse so that stack traversal is in correct layout order)
+ for (int32_t i = 0; i < currentElement->children.length; i++) {
+ context->treeNodeVisited.internalArray[dfsBuffer.length] = false;
+ Clay__LayoutElementTreeNodeArray_Add(&dfsBuffer, CLAY__INIT(Clay__LayoutElementTreeNode) { .layoutElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]) });
+ }
+ continue;
+ }
+ dfsBuffer.length--;
+
+ // DFS node has been visited, this is on the way back up to the root
+ Clay_LayoutConfig *layoutConfig = ¤tElement->config.layout;
+ if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+ // Resize any parent containers that have grown in height along their non layout axis
+ for (int32_t j = 0; j < currentElement->children.length; ++j) {
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[j]);
+ float childHeightWithPadding = CLAY__MAX(childElement->dimensions.height + layoutConfig->padding.top + layoutConfig->padding.bottom, currentElement->dimensions.height);
+ currentElement->dimensions.height = CLAY__MIN(CLAY__MAX(childHeightWithPadding, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max);
+ }
+ } else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) {
+ // Resizing along the layout axis
+ float contentHeight = (float)(layoutConfig->padding.top + layoutConfig->padding.bottom);
+ for (int32_t j = 0; j < currentElement->children.length; ++j) {
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[j]);
+ contentHeight += childElement->dimensions.height;
+ }
+ contentHeight += (float)(CLAY__MAX(currentElement->children.length - 1, 0) * layoutConfig->childGap);
+ currentElement->dimensions.height = CLAY__MIN(CLAY__MAX(contentHeight, layoutConfig->sizing.height.size.minMax.min), layoutConfig->sizing.height.size.minMax.max);
+ }
+ }
+
+ // Calculate sizing along the Y axis
+ Clay__SizeContainersAlongAxis(false, deltaTime, NULL, NULL);
+
+ // Scale horizontal widths according to aspect ratio
+ for (int32_t i = 0; i < aspectRatioElements.length; ++i) {
+ Clay_LayoutElement* aspectElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&aspectRatioElements, i));
+ aspectElement->dimensions.width = aspectElement->config.aspectRatio.aspectRatio * aspectElement->dimensions.height;
+ }
+
+ // Sort tree roots by z-index
+ int32_t sortMax = context->layoutElementTreeRoots.length - 1;
+ while (sortMax > 0) { // todo dumb bubble sort
+ for (int32_t i = 0; i < sortMax; ++i) {
+ Clay__LayoutElementTreeRoot current = *Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, i);
+ Clay__LayoutElementTreeRoot next = *Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, i + 1);
+ if (next.zIndex < current.zIndex) {
+ Clay__LayoutElementTreeRootArray_Set(&context->layoutElementTreeRoots, i, next);
+ Clay__LayoutElementTreeRootArray_Set(&context->layoutElementTreeRoots, i + 1, current);
+ }
+ }
+ sortMax--;
+ }
+
+ // Calculate final positions and generate render commands
+ context->renderCommands.length = 0;
+ dfsBuffer.length = 0;
+
+ for (int32_t rootIndex = 0; rootIndex < context->layoutElementTreeRoots.length; ++rootIndex) {
+ dfsBuffer.length = 0;
+ Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex);
+ Clay_LayoutElement *rootElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)root->layoutElementIndex);
+ Clay_Vector2 rootPosition = CLAY__DEFAULT_STRUCT;
+ Clay_LayoutElementHashMapItem *parentHashMapItem = Clay__GetHashMapItem(root->parentId);
+ // Position root floating containers
+ if (rootElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE && parentHashMapItem) {
+ Clay_FloatingElementConfig *config = &rootElement->config.floating;
+ Clay_Dimensions rootDimensions = rootElement->dimensions;
+ Clay_BoundingBox parentBoundingBox = parentHashMapItem->boundingBox;
+ // Set X position
+ Clay_Vector2 targetAttachPosition = CLAY__DEFAULT_STRUCT;
+ switch (config->attachPoints.parent) {
+ case CLAY_ATTACH_POINT_LEFT_TOP:
+ case CLAY_ATTACH_POINT_LEFT_CENTER:
+ case CLAY_ATTACH_POINT_LEFT_BOTTOM: targetAttachPosition.x = parentBoundingBox.x; break;
+ case CLAY_ATTACH_POINT_CENTER_TOP:
+ case CLAY_ATTACH_POINT_CENTER_CENTER:
+ case CLAY_ATTACH_POINT_CENTER_BOTTOM: targetAttachPosition.x = parentBoundingBox.x + (parentBoundingBox.width / 2); break;
+ case CLAY_ATTACH_POINT_RIGHT_TOP:
+ case CLAY_ATTACH_POINT_RIGHT_CENTER:
+ case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.x = parentBoundingBox.x + parentBoundingBox.width; break;
+ }
+ switch (config->attachPoints.element) {
+ case CLAY_ATTACH_POINT_LEFT_TOP:
+ case CLAY_ATTACH_POINT_LEFT_CENTER:
+ case CLAY_ATTACH_POINT_LEFT_BOTTOM: break;
+ case CLAY_ATTACH_POINT_CENTER_TOP:
+ case CLAY_ATTACH_POINT_CENTER_CENTER:
+ case CLAY_ATTACH_POINT_CENTER_BOTTOM: targetAttachPosition.x -= (rootDimensions.width / 2); break;
+ case CLAY_ATTACH_POINT_RIGHT_TOP:
+ case CLAY_ATTACH_POINT_RIGHT_CENTER:
+ case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.x -= rootDimensions.width; break;
+ }
+ switch (config->attachPoints.parent) { // I know I could merge the x and y switch statements, but this is easier to read
+ case CLAY_ATTACH_POINT_LEFT_TOP:
+ case CLAY_ATTACH_POINT_RIGHT_TOP:
+ case CLAY_ATTACH_POINT_CENTER_TOP: targetAttachPosition.y = parentBoundingBox.y; break;
+ case CLAY_ATTACH_POINT_LEFT_CENTER:
+ case CLAY_ATTACH_POINT_CENTER_CENTER:
+ case CLAY_ATTACH_POINT_RIGHT_CENTER: targetAttachPosition.y = parentBoundingBox.y + (parentBoundingBox.height / 2); break;
+ case CLAY_ATTACH_POINT_LEFT_BOTTOM:
+ case CLAY_ATTACH_POINT_CENTER_BOTTOM:
+ case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.y = parentBoundingBox.y + parentBoundingBox.height; break;
+ }
+ switch (config->attachPoints.element) {
+ case CLAY_ATTACH_POINT_LEFT_TOP:
+ case CLAY_ATTACH_POINT_RIGHT_TOP:
+ case CLAY_ATTACH_POINT_CENTER_TOP: break;
+ case CLAY_ATTACH_POINT_LEFT_CENTER:
+ case CLAY_ATTACH_POINT_CENTER_CENTER:
+ case CLAY_ATTACH_POINT_RIGHT_CENTER: targetAttachPosition.y -= (rootDimensions.height / 2); break;
+ case CLAY_ATTACH_POINT_LEFT_BOTTOM:
+ case CLAY_ATTACH_POINT_CENTER_BOTTOM:
+ case CLAY_ATTACH_POINT_RIGHT_BOTTOM: targetAttachPosition.y -= rootDimensions.height; break;
+ }
+ targetAttachPosition.x += config->offset.x;
+ targetAttachPosition.y += config->offset.y;
+ rootPosition = targetAttachPosition;
+ }
+ if (root->clipElementId) {
+ Clay_LayoutElementHashMapItem *clipHashMapItem = Clay__GetHashMapItem(root->clipElementId);
+ if (clipHashMapItem && !Clay__ElementIsOffscreen(&clipHashMapItem->boundingBox)) {
+ // Floating elements that are attached to scrolling contents won't be correctly positioned if external scroll handling is enabled, fix here
+ if (context->externalScrollHandlingEnabled) {
+ if (clipHashMapItem->layoutElement->config.clip.horizontal) {
+ rootPosition.x += clipHashMapItem->layoutElement->config.clip.childOffset.x;
+ }
+ if (clipHashMapItem->layoutElement->config.clip.vertical) {
+ rootPosition.y += clipHashMapItem->layoutElement->config.clip.childOffset.y;
+ }
+ }
+ if (generateRenderCommands) {
+ Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+ .boundingBox = clipHashMapItem->boundingBox,
+ .userData = 0,
+ .id = Clay__HashNumber(rootElement->id, rootElement->children.length + 10).id, // TODO need a better strategy for managing derived ids
+ .zIndex = root->zIndex,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_START,
+ });
+ }
+ }
+ }
+ Clay__LayoutElementTreeNodeArray_Add(&dfsBuffer, CLAY__INIT(Clay__LayoutElementTreeNode) { .layoutElement = rootElement, .position = rootPosition, .nextChildOffset = { .x = (float)rootElement->config.layout.padding.left, .y = (float)rootElement->config.layout.padding.top } });
+
+ context->treeNodeVisited.internalArray[0] = false;
+ while (dfsBuffer.length > 0) {
+ Clay__LayoutElementTreeNode *currentElementTreeNode = Clay__LayoutElementTreeNodeArray_Get(&dfsBuffer, (int)dfsBuffer.length - 1);
+ Clay_LayoutElement *currentElement = currentElementTreeNode->layoutElement;
+ Clay_LayoutConfig *layoutConfig = currentElement->isTextElement ? &CLAY_LAYOUT_DEFAULT : ¤tElement->config.layout;
+ Clay_Vector2 scrollOffset = CLAY__DEFAULT_STRUCT;
+
+ // DFS is returning back upwards
+ if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
+ if (currentElement->isTextElement) {
+ dfsBuffer.length--;
+ continue;
+ }
+ Clay_LayoutElementHashMapItem *currentElementData = Clay__GetHashMapItem(currentElement->id);
+ if (generateRenderCommands && !Clay__ElementIsOffscreen(¤tElementData->boundingBox)) {
+ // DFS is returning upwards backwards
+ bool closeClipElement = false;
+ if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) {
+ closeClipElement = true;
+ for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+ Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+ if (mapping->layoutElement == currentElement) {
+ scrollOffset = currentElement->config.clip.childOffset;
+ if (context->externalScrollHandlingEnabled) {
+ scrollOffset = CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT;
+ }
+ break;
+ }
+ }
+ }
+
+ if (Clay__BorderHasAnyWidth(¤tElement->config.border)) {
+ Clay_BoundingBox currentElementBoundingBox = currentElementData->boundingBox;
+ Clay_BorderElementConfig *borderConfig = ¤tElement->config.border;
+ Clay_RenderCommand renderCommand = {
+ .boundingBox = currentElementBoundingBox,
+ .renderData = { .border = {
+ .color = borderConfig->color,
+ .cornerRadius = currentElement->config.cornerRadius,
+ .width = borderConfig->width
+ }},
+ .userData = currentElement->config.userData,
+ .id = Clay__HashNumber(currentElement->id, currentElement->children.length).id,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_BORDER,
+ };
+ Clay__AddRenderCommand(renderCommand);
+ if (borderConfig->width.betweenChildren > 0 && borderConfig->color.a > 0) {
+ float halfGap = layoutConfig->childGap / 2;
+ float halfWidth = borderConfig->width.betweenChildren / 2;
+ Clay_Vector2 borderOffset = { (float)layoutConfig->padding.left - halfGap, (float)layoutConfig->padding.top - halfGap };
+ if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+ for (int32_t i = 0; i < currentElement->children.length; ++i) {
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+ if (i > 0) {
+ Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+ .boundingBox = { currentElementBoundingBox.x + borderOffset.x + scrollOffset.x - halfWidth, currentElementBoundingBox.y + scrollOffset.y, (float)borderConfig->width.betweenChildren, currentElement->dimensions.height },
+ .renderData = { .rectangle = {
+ .backgroundColor = borderConfig->color,
+ } },
+ .userData = currentElement->config.userData,
+ .id = Clay__HashNumber(currentElement->id, currentElement->children.length + 1 + i).id,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+ });
+ }
+ borderOffset.x += (childElement->dimensions.width + (float)layoutConfig->childGap);
+ }
+ } else {
+ for (int32_t i = 0; i < currentElement->children.length; ++i) {
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+ if (i > 0) {
+ Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+ .boundingBox = { currentElementBoundingBox.x + scrollOffset.x, currentElementBoundingBox.y + borderOffset.y + scrollOffset.y - halfWidth, currentElement->dimensions.width, (float)borderConfig->width.betweenChildren },
+ .renderData = { .rectangle = {
+ .backgroundColor = borderConfig->color,
+ } },
+ .userData = currentElement->config.userData,
+ .id = Clay__HashNumber(currentElement->id, currentElement->children.length + 1 + i).id,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+ });
+ }
+ borderOffset.y += (childElement->dimensions.height + (float)layoutConfig->childGap);
+ }
+ }
+ }
+ }
+ if (currentElement->config.overlayColor.a > 0) {
+ Clay_RenderCommand renderCommand = {
+ .userData = currentElement->config.userData,
+ .id = currentElement->id,
+ .zIndex = root->zIndex,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END,
+ };
+ Clay__AddRenderCommand(renderCommand);
+ }
+ // This exists because the scissor needs to end _after_ borders between elements
+ if (closeClipElement) {
+ Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+ .id = Clay__HashNumber(currentElement->id, rootElement->children.length + 11).id,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_END,
+ });
+ }
+ }
+
+ dfsBuffer.length--;
+ continue;
+ }
+
+ // This will only be run a single time for each element in downwards DFS order
+ context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
+ Clay_BoundingBox currentElementBoundingBox = { currentElementTreeNode->position.x, currentElementTreeNode->position.y, currentElement->dimensions.width, currentElement->dimensions.height };
+ Clay__ScrollContainerDataInternal *scrollContainerData = CLAY__NULL;
+ if (!currentElement->isTextElement) {
+ if (useStoredBoundingBoxes && currentElement->config.transition.handler) {
+ bool found = false;
+ for (int j = 0; j < context->transitionDatas.length; ++j) {
+ Clay__TransitionDataInternal* transitionData = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, j);
+ if (transitionData->elementId == currentElement->id) {
+ found = true;
+ if (transitionData->state != CLAY_TRANSITION_STATE_IDLE) {
+ if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_X) != 0) currentElementBoundingBox.x = transitionData->currentState.boundingBox.x;
+ if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_Y) != 0) currentElementBoundingBox.y = transitionData->currentState.boundingBox.y;
+ if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_WIDTH) != 0) currentElementBoundingBox.width = transitionData->currentState.boundingBox.width;
+ if ((transitionData->activeProperties & CLAY_TRANSITION_PROPERTY_HEIGHT) != 0) currentElementBoundingBox.height = transitionData->currentState.boundingBox.height;
+ }
+ break;
+ }
+ }
+ // An exiting element that completed its transition this frame - skip tree
+ if (!found && currentElement->config.transition.exit.setFinalState) {
+ dfsBuffer.length--;
+ continue;
+ }
+ }
+ if (currentElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+ Clay_FloatingElementConfig *floatingElementConfig = ¤tElement->config.floating;
+ Clay_Dimensions expand = floatingElementConfig->expand;
+ currentElementBoundingBox.x -= expand.width;
+ currentElementBoundingBox.width += expand.width * 2;
+ currentElementBoundingBox.y -= expand.height;
+ currentElementBoundingBox.height += expand.height * 2;
+ }
+
+ // Apply scroll offsets to container
+ if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) {
+ // This linear scan could theoretically be slow under very strange conditions, but I can't imagine a real UI with more than a few 10's of scroll containers
+ for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+ Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+ if (mapping->layoutElement == currentElement) {
+ scrollContainerData = mapping;
+ mapping->boundingBox = currentElementBoundingBox;
+ scrollOffset = currentElement->config.clip.childOffset;
+ if (context->externalScrollHandlingEnabled) {
+ scrollOffset = CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ bool offscreen = Clay__ElementIsOffscreen(¤tElementBoundingBox);
+
+ // Generate render commands for current element
+ if (generateRenderCommands && !offscreen) {
+ if (currentElement->isTextElement) {
+ Clay_TextElementConfig *textElementConfig = ¤tElement->textConfig;
+ float naturalLineHeight = currentElement->textElementData.preferredDimensions.height;
+ float finalLineHeight = textElementConfig->lineHeight > 0 ? (float)textElementConfig->lineHeight : naturalLineHeight;
+ float lineHeightOffset = (finalLineHeight - naturalLineHeight) / 2;
+ float yPosition = lineHeightOffset;
+ for (int32_t lineIndex = 0; lineIndex < currentElement->textElementData.wrappedLines.length; ++lineIndex) {
+ Clay__WrappedTextLine *wrappedLine = Clay__WrappedTextLineArraySlice_Get(¤tElement->textElementData.wrappedLines, lineIndex);
+ if (wrappedLine->line.length == 0) {
+ yPosition += finalLineHeight;
+ continue;
+ }
+ float offset = (currentElementBoundingBox.width - wrappedLine->dimensions.width);
+ if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_LEFT) {
+ offset = 0;
+ }
+ if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_CENTER) {
+ offset /= 2;
+ }
+ Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+ .boundingBox = { currentElementBoundingBox.x + offset, currentElementBoundingBox.y + yPosition, wrappedLine->dimensions.width, wrappedLine->dimensions.height },
+ .renderData = { .text = {
+ .stringContents = CLAY__INIT(Clay_StringSlice) { .length = wrappedLine->line.length, .chars = wrappedLine->line.chars, .baseChars = currentElement->textElementData.text.chars },
+ .textColor = textElementConfig->textColor,
+ .fontId = textElementConfig->fontId,
+ .fontSize = textElementConfig->fontSize,
+ .letterSpacing = textElementConfig->letterSpacing,
+ .lineHeight = textElementConfig->lineHeight,
+ }},
+ .userData = textElementConfig->userData,
+ .id = Clay__HashNumber(lineIndex, currentElement->id).id,
+ .zIndex = root->zIndex,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT,
+ });
+ yPosition += finalLineHeight;
+
+ if (!context->disableCulling && (currentElementBoundingBox.y + yPosition > context->layoutDimensions.height)) {
+ break;
+ }
+ }
+ } else {
+ if (currentElement->config.overlayColor.a > 0) {
+ Clay_RenderCommand renderCommand = {
+ .renderData = {
+ .overlayColor = { .color = currentElement->config.overlayColor }
+ },
+ .userData = currentElement->config.userData,
+ .id = currentElement->id,
+ .zIndex = root->zIndex,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START,
+ };
+ Clay__AddRenderCommand(renderCommand);
+ }
+ if (currentElement->config.image.imageData) {
+ Clay_RenderCommand renderCommand = {
+ .boundingBox = currentElementBoundingBox,
+ .renderData = {
+ .image = {
+ .backgroundColor = currentElement->config.backgroundColor,
+ .cornerRadius = currentElement->config.cornerRadius,
+ .imageData = currentElement->config.image.imageData,
+ }
+ },
+ .userData = currentElement->config.userData,
+ .id = currentElement->id,
+ .zIndex = root->zIndex,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_IMAGE,
+ };
+ Clay__AddRenderCommand(renderCommand);
+ }
+ if (currentElement->config.custom.customData) {
+ Clay_RenderCommand renderCommand = {
+ .boundingBox = currentElementBoundingBox,
+ .renderData = {
+ .custom = {
+ .backgroundColor = currentElement->config.backgroundColor,
+ .cornerRadius = currentElement->config.cornerRadius,
+ .customData = currentElement->config.custom.customData,
+ }
+ },
+ .userData = currentElement->config.userData,
+ .id = currentElement->id,
+ .zIndex = root->zIndex,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_CUSTOM,
+ };
+ Clay__AddRenderCommand(renderCommand);
+ }
+ if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) {
+ Clay_RenderCommand renderCommand = {
+ .boundingBox = currentElementBoundingBox,
+ .renderData = {
+ .clip = {
+ .horizontal = currentElement->config.clip.horizontal,
+ .vertical = currentElement->config.clip.vertical,
+ }
+ },
+ .userData = currentElement->config.userData,
+ .id = currentElement->id,
+ .zIndex = root->zIndex,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_START,
+ };
+ Clay__AddRenderCommand(renderCommand);
+ }
+ if (currentElement->config.backgroundColor.a > 0) {
+ Clay_RenderCommand renderCommand = {
+ .boundingBox = currentElementBoundingBox,
+ .renderData = { .rectangle = {
+ .backgroundColor = currentElement->config.backgroundColor,
+ .cornerRadius = currentElement->config.cornerRadius,
+ } },
+ .userData = currentElement->config.userData,
+ .id = currentElement->id,
+ .zIndex = root->zIndex,
+ .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+ };
+ Clay__AddRenderCommand(renderCommand);
+ }
+ }
+ }
+
+ Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(currentElement->id);
+ hashMapItem->boundingBox = currentElementBoundingBox;
+
+ if (currentElement->isTextElement) continue;
+
+ // Setup positions for child elements and add to DFS buffer ----------
+
+ // On-axis alignment
+ Clay_Dimensions contentSizeCurrent = {};
+ if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+ for (int32_t i = 0; i < currentElement->children.length; ++i) {
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+ if (childElement->exiting) continue;
+ contentSizeCurrent.width += childElement->dimensions.width;
+ contentSizeCurrent.height = CLAY__MAX(contentSizeCurrent.height, childElement->dimensions.height);
+ }
+ contentSizeCurrent.width += (float)(CLAY__MAX(currentElement->children.length - 1, 0) * layoutConfig->childGap);
+ float extraSpace = currentElement->dimensions.width - (float)(layoutConfig->padding.left + layoutConfig->padding.right) - contentSizeCurrent.width;
+ switch (layoutConfig->childAlignment.x) {
+ case CLAY_ALIGN_X_LEFT: extraSpace = 0; break;
+ case CLAY_ALIGN_X_CENTER: extraSpace /= 2; break;
+ default: break;
+ }
+ extraSpace = CLAY__MAX(0, extraSpace);
+ currentElementTreeNode->nextChildOffset.x += extraSpace;
+ } else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) {
+ for (int32_t i = 0; i < currentElement->children.length; ++i) {
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+ if (childElement->exiting) continue;
+ contentSizeCurrent.width = CLAY__MAX(contentSizeCurrent.width, childElement->dimensions.width);
+ contentSizeCurrent.height += childElement->dimensions.height;
+ }
+ contentSizeCurrent.height += (float)(CLAY__MAX(currentElement->children.length - 1, 0) * layoutConfig->childGap);
+ float extraSpace = currentElement->dimensions.height - (float)(layoutConfig->padding.top + layoutConfig->padding.bottom) - contentSizeCurrent.height;
+ switch (layoutConfig->childAlignment.y) {
+ case CLAY_ALIGN_Y_TOP: extraSpace = 0; break;
+ case CLAY_ALIGN_Y_CENTER: extraSpace /= 2; break;
+ default: break;
+ }
+ extraSpace = CLAY__MAX(0, extraSpace);
+ currentElementTreeNode->nextChildOffset.y += extraSpace;
+ }
+
+ if (scrollContainerData) {
+ scrollContainerData->contentSize = CLAY__INIT(Clay_Dimensions) {contentSizeCurrent.width + (float)(layoutConfig->padding.left + layoutConfig->padding.right), contentSizeCurrent.height + (float)(layoutConfig->padding.top + layoutConfig->padding.bottom) };
+ }
+
+ // Add children to the DFS buffer
+ dfsBuffer.length += currentElement->children.length;
+ for (int32_t i = 0; i < currentElement->children.length; ++i) {
+ Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->children.elements[i]);
+ Clay_LayoutElementHashMapItem* childMapItem = Clay__GetHashMapItem(childElement->id);
+ // Alignment along non layout axis
+ if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+ currentElementTreeNode->nextChildOffset.y = currentElement->config.layout.padding.top;
+ float whiteSpaceAroundChild = currentElement->dimensions.height - (float)(layoutConfig->padding.top + layoutConfig->padding.bottom) - childElement->dimensions.height;
+ switch (layoutConfig->childAlignment.y) {
+ case CLAY_ALIGN_Y_TOP: break;
+ case CLAY_ALIGN_Y_CENTER: currentElementTreeNode->nextChildOffset.y += whiteSpaceAroundChild / 2; break;
+ case CLAY_ALIGN_Y_BOTTOM: currentElementTreeNode->nextChildOffset.y += whiteSpaceAroundChild; break;
+ }
+ } else {
+ currentElementTreeNode->nextChildOffset.x = currentElement->config.layout.padding.left;
+ float whiteSpaceAroundChild = currentElement->dimensions.width - (float)(layoutConfig->padding.left + layoutConfig->padding.right) - childElement->dimensions.width;
+ switch (layoutConfig->childAlignment.x) {
+ case CLAY_ALIGN_X_LEFT: break;
+ case CLAY_ALIGN_X_CENTER: currentElementTreeNode->nextChildOffset.x += whiteSpaceAroundChild / 2; break;
+ case CLAY_ALIGN_X_RIGHT: currentElementTreeNode->nextChildOffset.x += whiteSpaceAroundChild; break;
+ }
+ }
+
+ Clay_Vector2 childPosition = {
+ currentElementBoundingBox.x + currentElementTreeNode->nextChildOffset.x + scrollOffset.x,
+ currentElementBoundingBox.y + currentElementTreeNode->nextChildOffset.y + scrollOffset.y,
+ };
+
+ // DFS buffer elements need to be added in reverse because stack traversal happens backwards
+ uint32_t newNodeIndex = dfsBuffer.length - 1 - i;
+ dfsBuffer.internalArray[newNodeIndex] = CLAY__INIT(Clay__LayoutElementTreeNode) {
+ .layoutElement = childElement,
+ .position = CLAY__INIT(Clay_Vector2) { childPosition.x, childPosition.y },
+ .nextChildOffset = { .x = (float)childElement->config.layout.padding.left, .y = (float)childElement->config.layout.padding.top },
+ };
+ context->treeNodeVisited.internalArray[newNodeIndex] = false;
+
+ // Update parent offsets
+ if (!childElement->exiting) {
+ if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+ currentElementTreeNode->nextChildOffset.x += childElement->dimensions.width + (float)layoutConfig->childGap;
+ } else {
+ currentElementTreeNode->nextChildOffset.y += childElement->dimensions.height + (float)layoutConfig->childGap;
+ }
+ }
+ }
+ }
+
+ if (root->clipElementId) {
+ Clay_LayoutElementHashMapItem *clipHashMapItem = Clay__GetHashMapItem(root->clipElementId);
+ if (clipHashMapItem && !Clay__ElementIsOffscreen(&clipHashMapItem->boundingBox)) {
+ Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) { .id = Clay__HashNumber(rootElement->id, rootElement->children.length + 11).id, .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_END });
+ }
+ }
+ }
+}
+
+CLAY_WASM_EXPORT("Clay_GetPointerOverIds")
+CLAY_DLL_EXPORT Clay_ElementIdArray Clay_GetPointerOverIds(void) {
+ return Clay_GetCurrentContext()->pointerOverIds;
+}
+
+#pragma region DebugTools
+Clay_Color CLAY__DEBUGVIEW_COLOR_1 = {58, 56, 52, 255};
+Clay_Color CLAY__DEBUGVIEW_COLOR_2 = {62, 60, 58, 255};
+Clay_Color CLAY__DEBUGVIEW_COLOR_3 = {141, 133, 135, 255};
+Clay_Color CLAY__DEBUGVIEW_COLOR_4 = {238, 226, 231, 255};
+Clay_Color CLAY__DEBUGVIEW_COLOR_SELECTED_ROW = {102, 80, 78, 255};
+const int32_t CLAY__DEBUGVIEW_ROW_HEIGHT = 30;
+const int32_t CLAY__DEBUGVIEW_OUTER_PADDING = 10;
+const int32_t CLAY__DEBUGVIEW_INDENT_WIDTH = 16;
+Clay_TextElementConfig Clay__DebugView_TextNameConfig = {.textColor = {238, 226, 231, 255}, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE };
+Clay_LayoutConfig Clay__DebugView_ScrollViewItemLayoutConfig = CLAY__DEFAULT_STRUCT;
+
+typedef struct {
+ Clay_String label;
+ Clay_Color color;
+} Clay__DebugElementConfigTypeLabelConfig;
+
+typedef enum {
+ CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR,
+ CLAY__ELEMENT_CONFIG_TYPE_OVERLAY_COLOR,
+ CLAY__ELEMENT_CONFIG_TYPE_CORNER_RADIUS,
+ CLAY__ELEMENT_CONFIG_TYPE_TEXT,
+ CLAY__ELEMENT_CONFIG_TYPE_ASPECT,
+ CLAY__ELEMENT_CONFIG_TYPE_IMAGE,
+ CLAY__ELEMENT_CONFIG_TYPE_FLOATING,
+ CLAY__ELEMENT_CONFIG_TYPE_CLIP,
+ CLAY__ELEMENT_CONFIG_TYPE_BORDER,
+ CLAY__ELEMENT_CONFIG_TYPE_CUSTOM,
+} Clay__DebugElementConfigType;
+
+Clay__DebugElementConfigTypeLabelConfig Clay__DebugGetElementConfigTypeLabel(Clay__DebugElementConfigType type) {
+ switch (type) {
+ case CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Background"), {243,134,48,255} };
+ case CLAY__ELEMENT_CONFIG_TYPE_OVERLAY_COLOR: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Overlay"), { 142,129,206, 255} };
+ case CLAY__ELEMENT_CONFIG_TYPE_CORNER_RADIUS: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) {CLAY_STRING("Radius"), {239,148,157, 255 } };
+ case CLAY__ELEMENT_CONFIG_TYPE_TEXT: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Text"), {105,210,231,255} };
+ case CLAY__ELEMENT_CONFIG_TYPE_ASPECT: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Aspect"), {101,149,194,255} };
+ case CLAY__ELEMENT_CONFIG_TYPE_IMAGE: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Image"), {121,189,154,255} };
+ case CLAY__ELEMENT_CONFIG_TYPE_FLOATING: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Floating"), {250,105,0,255} };
+ case CLAY__ELEMENT_CONFIG_TYPE_CLIP: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) {CLAY_STRING("Scroll"), {242, 196, 90, 255} };
+ case CLAY__ELEMENT_CONFIG_TYPE_BORDER: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) {CLAY_STRING("Border"), {108, 91, 123, 255} };
+ case CLAY__ELEMENT_CONFIG_TYPE_CUSTOM: return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Custom"), {11,72,107,255} };
+ default: break;
+ }
+ return CLAY__INIT(Clay__DebugElementConfigTypeLabelConfig) { CLAY_STRING("Error"), {0,0,0,255} };
+}
+
+void Clay__RenderElementConfigTypeLabel(Clay_String label, Clay_Color color, bool offscreen) {
+ Clay_Color backgroundColor = color;
+ backgroundColor.a = 90;
+ CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = backgroundColor, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = color, .width = { 1, 1, 1, 1, 0 } } }) {
+ CLAY_TEXT(label, CLAY_TEXT_CONFIG({ .textColor = offscreen ? CLAY__DEBUGVIEW_COLOR_3 : CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+ }
+}
+
+typedef struct {
+ int32_t rowCount;
+ int32_t selectedElementRowIndex;
+} Clay__RenderDebugLayoutData;
+
+// Returns row count
+Clay__RenderDebugLayoutData Clay__RenderDebugLayoutElementsList(int32_t initialRootsLength, int32_t highlightedRowIndex) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ Clay__int32_tArray dfsBuffer = context->reusableElementIndexBuffer;
+ Clay__DebugView_ScrollViewItemLayoutConfig = CLAY__INIT(Clay_LayoutConfig) { .sizing = { .height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT) }, .childGap = 6, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }};
+ Clay__RenderDebugLayoutData layoutData = CLAY__DEFAULT_STRUCT;
+
+ uint32_t highlightedElementId = 0;
+
+ for (int32_t rootIndex = 0; rootIndex < initialRootsLength; ++rootIndex) {
+ dfsBuffer.length = 0;
+ Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex);
+ Clay__int32_tArray_Add(&dfsBuffer, (int32_t)root->layoutElementIndex);
+ context->treeNodeVisited.internalArray[0] = false;
+ if (rootIndex > 0) {
+ CLAY(CLAY_IDI("Clay__DebugView_EmptyRowOuter", rootIndex), { .layout = { .sizing = {.width = CLAY_SIZING_GROW(0)}, .padding = {CLAY__DEBUGVIEW_INDENT_WIDTH / 2, 0, 0, 0} } }) {
+ CLAY(CLAY_IDI("Clay__DebugView_EmptyRow", rootIndex), { .layout = { .sizing = { .width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED((float)CLAY__DEBUGVIEW_ROW_HEIGHT) }}, .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .top = 1 } } }) {}
+ }
+ layoutData.rowCount++;
+ }
+ while (dfsBuffer.length > 0) {
+ int32_t currentElementIndex = Clay__int32_tArray_GetValue(&dfsBuffer, (int)dfsBuffer.length - 1);
+ Clay_LayoutElement *currentElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)currentElementIndex);
+ if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
+ if (!currentElement->isTextElement && currentElement->children.length > 0) {
+ Clay__CloseElement();
+ Clay__CloseElement();
+ Clay__CloseElement();
+ }
+ dfsBuffer.length--;
+ continue;
+ }
+
+ if (currentElement->exiting) { // TODO there is a duplicate ID problem with exiting elements
+ dfsBuffer.length--;
+ continue;
+ }
+
+ if (highlightedRowIndex == layoutData.rowCount) {
+ if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+ context->debugSelectedElementId = currentElement->id;
+ }
+ highlightedElementId = currentElement->id;
+ }
+
+ context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
+ Clay_LayoutElementHashMapItem *currentElementData = Clay__GetHashMapItem(currentElement->id);
+ bool offscreen = Clay__ElementIsOffscreen(¤tElementData->boundingBox);
+ if (context->debugSelectedElementId == currentElement->id) {
+ layoutData.selectedElementRowIndex = layoutData.rowCount;
+ }
+ CLAY(CLAY_IDI("Clay__DebugView_ElementOuter", currentElement->id), { .layout = Clay__DebugView_ScrollViewItemLayoutConfig }) {
+ // Collapse icon / button
+ if (!(currentElement->isTextElement || currentElement->children.length == 0)) {
+ CLAY(CLAY_IDI("Clay__DebugView_CollapseElement", currentElement->id), {
+ .layout = { .sizing = {CLAY_SIZING_FIXED(16), CLAY_SIZING_FIXED(16)}, .childAlignment = { CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER} },
+ .cornerRadius = CLAY_CORNER_RADIUS(4),
+ .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = {1, 1, 1, 1, 0} },
+ }) {
+ CLAY_TEXT((currentElementData && currentElementData->debugData.collapsed) ? CLAY_STRING("+") : CLAY_STRING("-"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+ }
+ } else { // Square dot for empty containers
+ CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_FIXED(16), CLAY_SIZING_FIXED(16)}, .childAlignment = { CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER } } }) {
+ CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_FIXED(8), CLAY_SIZING_FIXED(8)} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_3, .cornerRadius = CLAY_CORNER_RADIUS(2) }) {}
+ }
+ }
+ // Collisions and offscreen info
+ if (currentElementData) {
+ if (currentElementData->debugData.collision) {
+ CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 }}, .border = { .color = {177, 147, 8, 255}, .width = {1, 1, 1, 1, 0} } }) {
+ CLAY_TEXT(CLAY_STRING("Duplicate ID"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 }));
+ }
+ }
+ if (offscreen) {
+ CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { 1, 1, 1, 1, 0} } }) {
+ CLAY_TEXT(CLAY_STRING("Offscreen"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 }));
+ }
+ }
+ }
+ if (currentElementData->elementId.stringId.length > 0) {
+ CLAY_AUTO_ID() {
+ Clay_TextElementConfig textConfig = offscreen ? CLAY__INIT(Clay_TextElementConfig) { .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 } : Clay__DebugView_TextNameConfig;
+ CLAY_TEXT(currentElementData->elementId.stringId, textConfig);
+ if (currentElementData->elementId.offset != 0) {
+ CLAY_TEXT(CLAY_STRING(" ("), textConfig);
+ CLAY_TEXT(Clay__IntToString(currentElementData->elementId.offset), textConfig);
+ CLAY_TEXT(CLAY_STRING(")"), textConfig);
+ }
+ }
+ }
+ if (currentElement->isTextElement) {
+ Clay__RenderElementConfigTypeLabel(CLAY_STRING("Text"), CLAY__INIT(Clay_Color) { 105,210,231,255 }, offscreen);
+ } else {
+ if (currentElement->config.backgroundColor.a > 0) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR);
+ Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+ }
+ if (currentElement->config.overlayColor.a > 0) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_OVERLAY_COLOR);
+ Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+ }
+ if (!Clay__MemCmp((const char*)¤tElement->config.cornerRadius, (const char*)&Clay__CornerRadius_DEFAULT, sizeof(Clay_CornerRadius))) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_CORNER_RADIUS);
+ Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+ }
+ if (currentElement->config.aspectRatio.aspectRatio != 0) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_ASPECT);
+ Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+ }
+ if (currentElement->config.image.imageData) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_IMAGE);
+ Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+ }
+ if (currentElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_FLOATING);
+ Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+ }
+ if (currentElement->config.clip.horizontal || currentElement->config.clip.vertical) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_CLIP);
+ Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+ }
+ if (Clay__BorderHasAnyWidth(¤tElement->config.border)) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_BORDER);
+ Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+ }
+ if (currentElement->config.custom.customData) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_CUSTOM);
+ Clay__RenderElementConfigTypeLabel(config.label, config.color, offscreen);
+ }
+ }
+ }
+
+ // Render the text contents below the element as a non-interactive row
+ if (currentElement->isTextElement) {
+ layoutData.rowCount++;
+ Clay__TextElementData *textElementData = ¤tElement->textElementData;
+ Clay_TextElementConfig rawTextConfig = offscreen ? CLAY__INIT(Clay_TextElementConfig) { .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 } : Clay__DebugView_TextNameConfig;
+ CLAY_AUTO_ID({ .layout = { .sizing = { .height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } } }) {
+ CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_INDENT_WIDTH + 16) } } }) {}
+ CLAY_TEXT(CLAY_STRING("\""), rawTextConfig);
+ CLAY_TEXT(textElementData->text.length > 40 ? (CLAY__INIT(Clay_String) { .length = 40, .chars = textElementData->text.chars }) : textElementData->text, rawTextConfig);
+ if (textElementData->text.length > 40) {
+ CLAY_TEXT(CLAY_STRING("..."), rawTextConfig);
+ }
+ CLAY_TEXT(CLAY_STRING("\""), rawTextConfig);
+ }
+ } else if (currentElement->children.length > 0) {
+ Clay__OpenElement();
+ Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { .layout = { .padding = { .left = 8 } } });
+ Clay__OpenElement();
+ Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { .layout = { .padding = { .left = CLAY__DEBUGVIEW_INDENT_WIDTH }}, .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .left = 1 } }});
+ Clay__OpenElement();
+ Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) { .layout = { .layoutDirection = CLAY_TOP_TO_BOTTOM } });
+ }
+
+ layoutData.rowCount++;
+ if (!(currentElement->isTextElement || (currentElementData && currentElementData->debugData.collapsed))) {
+ for (int32_t i = currentElement->children.length - 1; i >= 0; --i) {
+ Clay__int32_tArray_Add(&dfsBuffer, currentElement->children.elements[i]);
+ context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = false; // TODO needs to be ranged checked
+ }
+ }
+ }
+ }
+
+ if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+ Clay_ElementId collapseButtonId = Clay__HashString(CLAY_STRING("Clay__DebugView_CollapseElement"), 0);
+ for (int32_t i = (int)context->pointerOverIds.length - 1; i >= 0; i--) {
+ Clay_ElementId *elementId = Clay_ElementIdArray_Get(&context->pointerOverIds, i);
+ if (elementId->baseId == collapseButtonId.baseId) {
+ Clay_LayoutElementHashMapItem *highlightedItem = Clay__GetHashMapItem(elementId->offset);
+ highlightedItem->debugData.collapsed = !highlightedItem->debugData.collapsed;
+ break;
+ }
+ }
+ }
+
+ if (highlightedElementId) {
+ CLAY(CLAY_ID("Clay__DebugView_ElementHighlight"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .floating = { .parentId = highlightedElementId, .zIndex = 32767, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID } }) {
+ CLAY(CLAY_ID("Clay__DebugView_ElementHighlightRectangle"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .backgroundColor = Clay__debugViewHighlightColor }) {}
+ }
+ }
+ return layoutData;
+}
+
+void Clay__RenderDebugLayoutSizing(Clay_SizingAxis sizing, Clay_TextElementConfig infoTextConfig) {
+ Clay_String sizingLabel = CLAY_STRING("GROW");
+ if (sizing.type == CLAY__SIZING_TYPE_FIT) {
+ sizingLabel = CLAY_STRING("FIT");
+ } else if (sizing.type == CLAY__SIZING_TYPE_PERCENT) {
+ sizingLabel = CLAY_STRING("PERCENT");
+ } else if (sizing.type == CLAY__SIZING_TYPE_FIXED) {
+ sizingLabel = CLAY_STRING("FIXED");
+ }
+ CLAY_TEXT(sizingLabel, infoTextConfig);
+ if (sizing.type == CLAY__SIZING_TYPE_GROW || sizing.type == CLAY__SIZING_TYPE_FIT || sizing.type == CLAY__SIZING_TYPE_FIXED) {
+ CLAY_TEXT(CLAY_STRING("("), infoTextConfig);
+ if (sizing.size.minMax.min != 0) {
+ CLAY_TEXT(CLAY_STRING("min: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(sizing.size.minMax.min), infoTextConfig);
+ if (sizing.size.minMax.max != CLAY__MAXFLOAT) {
+ CLAY_TEXT(CLAY_STRING(", "), infoTextConfig);
+ }
+ }
+ if (sizing.size.minMax.max != CLAY__MAXFLOAT) {
+ CLAY_TEXT(CLAY_STRING("max: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(sizing.size.minMax.max), infoTextConfig);
+ }
+ CLAY_TEXT(CLAY_STRING(")"), infoTextConfig);
+ } else if (sizing.type == CLAY__SIZING_TYPE_PERCENT) {
+ CLAY_TEXT(CLAY_STRING("("), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(sizing.size.percent * 100), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING("%)"), infoTextConfig);
+ }
+}
+
+void Clay__DebugViewRenderElementConfigHeader(Clay_String elementId, Clay__DebugElementConfigType type) {
+ Clay__DebugElementConfigTypeLabelConfig config = Clay__DebugGetElementConfigTypeLabel(type);
+ Clay_Color backgroundColor = config.color;
+ backgroundColor.a = 90;
+ CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = backgroundColor, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = config.color, .width = { 1, 1, 1, 1, 0 } } }) {
+ CLAY_TEXT(config.label, CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+ }
+}
+
+void Clay__RenderDebugViewColor(Clay_Color color, Clay_TextElementConfig textConfig) {
+ CLAY_AUTO_ID({ .layout = { .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+ CLAY_TEXT(CLAY_STRING("{ r: "), textConfig);
+ CLAY_TEXT(Clay__IntToString(color.r), textConfig);
+ CLAY_TEXT(CLAY_STRING(", g: "), textConfig);
+ CLAY_TEXT(Clay__IntToString(color.g), textConfig);
+ CLAY_TEXT(CLAY_STRING(", b: "), textConfig);
+ CLAY_TEXT(Clay__IntToString(color.b), textConfig);
+ CLAY_TEXT(CLAY_STRING(", a: "), textConfig);
+ CLAY_TEXT(Clay__IntToString(color.a), textConfig);
+ CLAY_TEXT(CLAY_STRING(" }"), textConfig);
+ CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_FIXED(10) } } }) {}
+ CLAY_AUTO_ID({ .layout = { .sizing = { CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 8), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 8)} }, .backgroundColor = color, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = CLAY__DEBUGVIEW_COLOR_4, .width = { 1, 1, 1, 1, 0 } } }) {}
+ }
+}
+
+void Clay__RenderDebugViewCornerRadius(Clay_CornerRadius cornerRadius, Clay_TextElementConfig textConfig) {
+ CLAY_AUTO_ID({ .layout = { .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+ CLAY_TEXT(CLAY_STRING("{ topLeft: "), textConfig);
+ CLAY_TEXT(Clay__IntToString(cornerRadius.topLeft), textConfig);
+ CLAY_TEXT(CLAY_STRING(", topRight: "), textConfig);
+ CLAY_TEXT(Clay__IntToString(cornerRadius.topRight), textConfig);
+ CLAY_TEXT(CLAY_STRING(", bottomLeft: "), textConfig);
+ CLAY_TEXT(Clay__IntToString(cornerRadius.bottomLeft), textConfig);
+ CLAY_TEXT(CLAY_STRING(", bottomRight: "), textConfig);
+ CLAY_TEXT(Clay__IntToString(cornerRadius.bottomRight), textConfig);
+ CLAY_TEXT(CLAY_STRING(" }"), textConfig);
+ }
+}
+
+void HandleDebugViewCloseButtonInteraction(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ (void) elementId; (void) pointerInfo; (void) userData;
+ if (pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+ context->debugModeEnabled = false;
+ }
+}
+
+void Clay__RenderDebugView(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ Clay_ElementId closeButtonId = Clay__HashString(CLAY_STRING("Clay__DebugViewTopHeaderCloseButtonOuter"), 0);
+ if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+ for (int32_t i = 0; i < context->pointerOverIds.length; ++i) {
+ Clay_ElementId *elementId = Clay_ElementIdArray_Get(&context->pointerOverIds, i);
+ if (elementId->id == closeButtonId.id) {
+ context->debugModeEnabled = false;
+ return;
+ }
+ }
+ }
+
+ uint32_t initialRootsLength = context->layoutElementTreeRoots.length;
+ uint32_t initialElementsLength = context->layoutElements.length;
+ Clay_TextElementConfig infoTextConfig = CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE });
+ Clay_TextElementConfig infoTitleConfig = CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE });
+ Clay_ElementId scrollId = Clay__HashString(CLAY_STRING("Clay__DebugViewOuterScrollPane"), 0);
+ float scrollYOffset = 0;
+ bool pointerInDebugView = context->pointerInfo.position.y < context->layoutDimensions.height - 300;
+ for (int32_t i = 0; i < context->scrollContainerDatas.length; ++i) {
+ Clay__ScrollContainerDataInternal *scrollContainerData = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+ if (scrollContainerData->elementId == scrollId.id) {
+ if (!context->externalScrollHandlingEnabled) {
+ scrollYOffset = scrollContainerData->scrollPosition.y;
+ } else {
+ pointerInDebugView = context->pointerInfo.position.y + scrollContainerData->scrollPosition.y < context->layoutDimensions.height - 300;
+ }
+ break;
+ }
+ }
+ int32_t highlightedRow = pointerInDebugView
+ ? (int32_t)((context->pointerInfo.position.y - scrollYOffset) / (float)CLAY__DEBUGVIEW_ROW_HEIGHT) - 1
+ : -1;
+ if (context->pointerInfo.position.x < context->layoutDimensions.width - (float)Clay__debugViewWidth) {
+ highlightedRow = -1;
+ }
+ Clay__RenderDebugLayoutData layoutData = CLAY__DEFAULT_STRUCT;
+ CLAY(CLAY_ID("Clay__DebugView"), {
+ .layout = { .sizing = { CLAY_SIZING_FIXED((float)Clay__debugViewWidth) , CLAY_SIZING_FIXED(context->layoutDimensions.height) }, .layoutDirection = CLAY_TOP_TO_BOTTOM },
+ .floating = { .zIndex = 32765, .attachPoints = { .element = CLAY_ATTACH_POINT_LEFT_CENTER, .parent = CLAY_ATTACH_POINT_RIGHT_CENTER }, .attachTo = CLAY_ATTACH_TO_ROOT, .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT },
+ .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .bottom = 1 } }
+ }) {
+ CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_2 }) {
+ CLAY_TEXT(CLAY_STRING("Clay Debug Tools"), infoTextConfig);
+ CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_GROW(0) } } }) {}
+ // Close button
+ CLAY_AUTO_ID({
+ .layout = { .sizing = {CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 10), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT - 10)}, .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER} },
+ .backgroundColor = {217,91,67,80},
+ .cornerRadius = CLAY_CORNER_RADIUS(4),
+ .border = { .color = { 217,91,67,255 }, .width = { 1, 1, 1, 1, 0 } },
+ }) {
+ Clay_OnHover(HandleDebugViewCloseButtonInteraction, 0);
+ CLAY_TEXT(CLAY_STRING("x"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+ }
+ }
+ CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(1)} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_3 } ) {}
+ CLAY(scrollId, { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .clip = { .horizontal = true, .vertical = true, .childOffset = Clay_GetScrollOffset() } }) {
+ CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = ((initialElementsLength + initialRootsLength) & 1) == 0 ? CLAY__DEBUGVIEW_COLOR_2 : CLAY__DEBUGVIEW_COLOR_1 }) {
+ Clay_ElementId panelContentsId = Clay__HashString(CLAY_STRING("Clay__DebugViewPaneOuter"), 0);
+ // Element list
+ CLAY(panelContentsId, { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)} }, .floating = { .zIndex = 32766, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, .attachTo = CLAY_ATTACH_TO_PARENT, .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT } }) {
+ CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)}, .padding = { CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+ layoutData = Clay__RenderDebugLayoutElementsList((int32_t)initialRootsLength, highlightedRow);
+ }
+ }
+ float contentWidth = Clay__GetHashMapItem(panelContentsId.id)->layoutElement->dimensions.width;
+ CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_FIXED(contentWidth) }, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {}
+ for (int32_t i = 0; i < layoutData.rowCount; i++) {
+ Clay_Color rowColor = (i & 1) == 0 ? CLAY__DEBUGVIEW_COLOR_2 : CLAY__DEBUGVIEW_COLOR_1;
+ if (i == layoutData.selectedElementRowIndex) {
+ rowColor = CLAY__DEBUGVIEW_COLOR_SELECTED_ROW;
+ }
+ if (i == highlightedRow) {
+ rowColor.r *= 1.25f;
+ rowColor.g *= 1.25f;
+ rowColor.b *= 1.25f;
+ }
+ CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = rowColor } ) {}
+ }
+ }
+ }
+ CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(1)} }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_3 }) {}
+ Clay_LayoutElementHashMapItem *selectedItem = Clay__GetHashMapItem(context->debugSelectedElementId);
+ if (selectedItem->layoutElement) {
+ CLAY_AUTO_ID({
+ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(300)}, .layoutDirection = CLAY_TOP_TO_BOTTOM },
+ .backgroundColor = CLAY__DEBUGVIEW_COLOR_2 ,
+ .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() },
+ .border = { .color = CLAY__DEBUGVIEW_COLOR_3, .width = { .betweenChildren = 1 } }
+ }) {
+ CLAY_AUTO_ID({ .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT + 8)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+ CLAY_TEXT(CLAY_STRING("Element Configuration"), infoTextConfig);
+ CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_GROW(0) } } }) {}
+ if (selectedItem->elementId.stringId.length != 0) {
+ CLAY_TEXT(selectedItem->elementId.stringId, infoTitleConfig);
+ if (selectedItem->elementId.offset != 0) {
+ CLAY_TEXT(CLAY_STRING(" ("), infoTitleConfig);
+ CLAY_TEXT(Clay__IntToString(selectedItem->elementId.offset), infoTitleConfig);
+ CLAY_TEXT(CLAY_STRING(")"), infoTitleConfig);
+ }
+ }
+ }
+ Clay_Padding attributeConfigPadding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 8, 8};
+ // Clay_LayoutConfig debug info
+ CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+ CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = { 200, 200, 200, 120 }, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = { 200, 200, 200, 255 }, .width = { 1, 1, 1, 1, 0 } } }) {
+ CLAY_TEXT(CLAY_STRING("Layout"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+ }
+ // .boundingBox
+ CLAY_TEXT(CLAY_STRING("Bounding Box"), infoTitleConfig);
+ CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+ CLAY_TEXT(CLAY_STRING("{ x: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.x), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", y: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.y), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", width: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.width), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", height: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(selectedItem->boundingBox.height), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+ }
+ if (!selectedItem->layoutElement->isTextElement) {
+ // .layoutDirection
+ CLAY_TEXT(CLAY_STRING("Layout Direction"), infoTitleConfig);
+ Clay_LayoutConfig *layoutConfig = &selectedItem->layoutElement->config.layout;
+ CLAY_TEXT(layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM ? CLAY_STRING("TOP_TO_BOTTOM") : CLAY_STRING("LEFT_TO_RIGHT"), infoTextConfig);
+ // .sizing
+ CLAY_TEXT(CLAY_STRING("Sizing"), infoTitleConfig);
+ CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+ CLAY_TEXT(CLAY_STRING("width: "), infoTextConfig);
+ Clay__RenderDebugLayoutSizing(layoutConfig->sizing.width, infoTextConfig);
+ }
+ CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+ CLAY_TEXT(CLAY_STRING("height: "), infoTextConfig);
+ Clay__RenderDebugLayoutSizing(layoutConfig->sizing.height, infoTextConfig);
+ }
+ // .padding
+ CLAY_TEXT(CLAY_STRING("Padding"), infoTitleConfig);
+ CLAY(CLAY_ID("Clay__DebugViewElementInfoPadding"), { }) {
+ CLAY_TEXT(CLAY_STRING("{ left: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(layoutConfig->padding.left), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", right: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(layoutConfig->padding.right), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", top: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(layoutConfig->padding.top), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", bottom: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(layoutConfig->padding.bottom), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+ }
+ // .childGap
+ CLAY_TEXT(CLAY_STRING("Child Gap"), infoTitleConfig);
+ CLAY_TEXT(Clay__IntToString(layoutConfig->childGap), infoTextConfig);
+ // .childAlignment
+ CLAY_TEXT(CLAY_STRING("Child Alignment"), infoTitleConfig);
+ CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+ CLAY_TEXT(CLAY_STRING("{ x: "), infoTextConfig);
+ Clay_String alignX = CLAY_STRING("LEFT");
+ if (layoutConfig->childAlignment.x == CLAY_ALIGN_X_CENTER) {
+ alignX = CLAY_STRING("CENTER");
+ } else if (layoutConfig->childAlignment.x == CLAY_ALIGN_X_RIGHT) {
+ alignX = CLAY_STRING("RIGHT");
+ }
+ CLAY_TEXT(alignX, infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", y: "), infoTextConfig);
+ Clay_String alignY = CLAY_STRING("TOP");
+ if (layoutConfig->childAlignment.y == CLAY_ALIGN_Y_CENTER) {
+ alignY = CLAY_STRING("CENTER");
+ } else if (layoutConfig->childAlignment.y == CLAY_ALIGN_Y_BOTTOM) {
+ alignY = CLAY_STRING("BOTTOM");
+ }
+ CLAY_TEXT(alignY, infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+ }
+ }
+ }
+ if (selectedItem->layoutElement->isTextElement) {
+ Clay_TextElementConfig *textConfig = &selectedItem->layoutElement->textConfig;
+ CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+ Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_TEXT);
+ // .fontSize
+ CLAY_TEXT(CLAY_STRING("Font Size"), infoTitleConfig);
+ CLAY_TEXT(Clay__IntToString(textConfig->fontSize), infoTextConfig);
+ // .fontId
+ CLAY_TEXT(CLAY_STRING("Font ID"), infoTitleConfig);
+ CLAY_TEXT(Clay__IntToString(textConfig->fontId), infoTextConfig);
+ // .lineHeight
+ CLAY_TEXT(CLAY_STRING("Line Height"), infoTitleConfig);
+ CLAY_TEXT(textConfig->lineHeight == 0 ? CLAY_STRING("auto") : Clay__IntToString(textConfig->lineHeight), infoTextConfig);
+ // .letterSpacing
+ CLAY_TEXT(CLAY_STRING("Letter Spacing"), infoTitleConfig);
+ CLAY_TEXT(Clay__IntToString(textConfig->letterSpacing), infoTextConfig);
+ // .wrapMode
+ CLAY_TEXT(CLAY_STRING("Wrap Mode"), infoTitleConfig);
+ Clay_String wrapMode = CLAY_STRING("WORDS");
+ if (textConfig->wrapMode == CLAY_TEXT_WRAP_NONE) {
+ wrapMode = CLAY_STRING("NONE");
+ } else if (textConfig->wrapMode == CLAY_TEXT_WRAP_NEWLINES) {
+ wrapMode = CLAY_STRING("NEWLINES");
+ }
+ CLAY_TEXT(wrapMode, infoTextConfig);
+ // .textAlignment
+ CLAY_TEXT(CLAY_STRING("Text Alignment"), infoTitleConfig);
+ Clay_String textAlignment = CLAY_STRING("LEFT");
+ if (textConfig->textAlignment == CLAY_TEXT_ALIGN_CENTER) {
+ textAlignment = CLAY_STRING("CENTER");
+ } else if (textConfig->textAlignment == CLAY_TEXT_ALIGN_RIGHT) {
+ textAlignment = CLAY_STRING("RIGHT");
+ }
+ CLAY_TEXT(textAlignment, infoTextConfig);
+ // .textColor
+ CLAY_TEXT(CLAY_STRING("Text Color"), infoTitleConfig);
+ Clay__RenderDebugViewColor(textConfig->textColor, infoTextConfig);
+ }
+ } else {
+ CLAY(CLAY_ID("Clay__DebugViewElementInfoSharedBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+ Clay__DebugElementConfigTypeLabelConfig labelConfig = Clay__DebugGetElementConfigTypeLabel(CLAY__ELEMENT_CONFIG_TYPE_BACKGROUND_COLOR);
+ Clay_Color backgroundColor = labelConfig.color;
+ backgroundColor.a = 90;
+ CLAY_AUTO_ID({ .layout = { .padding = { 8, 8, 2, 2 } }, .backgroundColor = backgroundColor, .cornerRadius = CLAY_CORNER_RADIUS(4), .border = { .color = labelConfig.color, .width = { 1, 1, 1, 1, 0 } } }) {
+ CLAY_TEXT(CLAY_STRING("Color & Radius"), CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16 }));
+ }
+ // .backgroundColor
+ if (selectedItem->layoutElement->config.backgroundColor.a > 0) {
+ CLAY_TEXT(CLAY_STRING("Background Color"), infoTitleConfig);
+ Clay__RenderDebugViewColor(selectedItem->layoutElement->config.backgroundColor, infoTextConfig);
+ }
+ // .cornerRadius
+ if (!Clay__MemCmp((const char*)&selectedItem->layoutElement->config.cornerRadius, (const char*)&Clay__CornerRadius_DEFAULT, sizeof(Clay_CornerRadius))) {
+ CLAY_TEXT(CLAY_STRING("Corner Radius"), infoTitleConfig);
+ Clay__RenderDebugViewCornerRadius(selectedItem->layoutElement->config.cornerRadius, infoTextConfig);
+ }
+ // .overlayColor
+ if (selectedItem->layoutElement->config.overlayColor.a > 0) {
+ CLAY_TEXT(CLAY_STRING("Overlay Color"), infoTitleConfig);
+ Clay__RenderDebugViewColor(selectedItem->layoutElement->config.overlayColor, infoTextConfig);
+ }
+ }
+ if (selectedItem->layoutElement->config.aspectRatio.aspectRatio > 0) {
+ Clay_AspectRatioElementConfig *aspectRatioConfig = &selectedItem->layoutElement->config.aspectRatio;
+ CLAY(CLAY_ID("Clay__DebugViewElementInfoAspectRatioBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+ Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_ASPECT);
+ CLAY_TEXT(CLAY_STRING("Aspect Ratio"), infoTitleConfig);
+ // Aspect Ratio
+ CLAY(CLAY_ID("Clay__DebugViewElementInfoAspectRatio"), { }) {
+ CLAY_TEXT(Clay__IntToString(aspectRatioConfig->aspectRatio), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING("."), infoTextConfig);
+ float frac = aspectRatioConfig->aspectRatio - (int)(aspectRatioConfig->aspectRatio);
+ frac *= 100;
+ if ((int)frac < 10) {
+ CLAY_TEXT(CLAY_STRING("0"), infoTextConfig);
+ }
+ CLAY_TEXT(Clay__IntToString(frac), infoTextConfig);
+ }
+ }
+ }
+ if (selectedItem->layoutElement->config.image.imageData) {
+ Clay_ImageElementConfig *imageConfig = &selectedItem->layoutElement->config.image;
+ Clay_AspectRatioElementConfig aspectConfig = { 1 };
+ if (selectedItem->layoutElement->config.aspectRatio.aspectRatio > 0) {
+ aspectConfig = selectedItem->layoutElement->config.aspectRatio;
+ }
+ CLAY(CLAY_ID("Clay__DebugViewElementInfoImageBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+ Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_IMAGE);
+ // Image Preview
+ CLAY_TEXT(CLAY_STRING("Preview"), infoTitleConfig);
+ CLAY_AUTO_ID({ .layout = { .sizing = { .width = CLAY_SIZING_GROW(64, 128), .height = CLAY_SIZING_GROW(64, 128) }}, .aspectRatio = aspectConfig, .image = *imageConfig }) {}
+ }
+ }
+ if (selectedItem->layoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE) {
+ Clay_FloatingElementConfig* floatingConfig = &selectedItem->layoutElement->config.floating;
+ CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+ Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_FLOATING);
+ // .offset
+ CLAY_TEXT(CLAY_STRING("Offset"), infoTitleConfig);
+ CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+ CLAY_TEXT(CLAY_STRING("{ x: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(floatingConfig->offset.x), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", y: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(floatingConfig->offset.y), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+ }
+ // .expand
+ CLAY_TEXT(CLAY_STRING("Expand"), infoTitleConfig);
+ CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+ CLAY_TEXT(CLAY_STRING("{ width: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(floatingConfig->expand.width), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", height: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(floatingConfig->expand.height), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+ }
+ // .zIndex
+ CLAY_TEXT(CLAY_STRING("z-index"), infoTitleConfig);
+ CLAY_TEXT(Clay__IntToString(floatingConfig->zIndex), infoTextConfig);
+ // .parentId
+ CLAY_TEXT(CLAY_STRING("Parent"), infoTitleConfig);
+ Clay_LayoutElementHashMapItem *hashItem = Clay__GetHashMapItem(floatingConfig->parentId);
+ CLAY_TEXT(hashItem->elementId.stringId, infoTextConfig);
+ // .attachPoints
+ CLAY_TEXT(CLAY_STRING("Attach Points"), infoTitleConfig);
+ CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+ CLAY_TEXT(CLAY_STRING("{ element: "), infoTextConfig);
+ Clay_String attachPointElement = CLAY_STRING("LEFT_TOP");
+ if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_LEFT_CENTER) {
+ attachPointElement = CLAY_STRING("LEFT_CENTER");
+ } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_LEFT_BOTTOM) {
+ attachPointElement = CLAY_STRING("LEFT_BOTTOM");
+ } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_CENTER_TOP) {
+ attachPointElement = CLAY_STRING("CENTER_TOP");
+ } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_CENTER_CENTER) {
+ attachPointElement = CLAY_STRING("CENTER_CENTER");
+ } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_CENTER_BOTTOM) {
+ attachPointElement = CLAY_STRING("CENTER_BOTTOM");
+ } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_RIGHT_TOP) {
+ attachPointElement = CLAY_STRING("RIGHT_TOP");
+ } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_RIGHT_CENTER) {
+ attachPointElement = CLAY_STRING("RIGHT_CENTER");
+ } else if (floatingConfig->attachPoints.element == CLAY_ATTACH_POINT_RIGHT_BOTTOM) {
+ attachPointElement = CLAY_STRING("RIGHT_BOTTOM");
+ }
+ CLAY_TEXT(attachPointElement, infoTextConfig);
+ Clay_String attachPointParent = CLAY_STRING("LEFT_TOP");
+ if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_LEFT_CENTER) {
+ attachPointParent = CLAY_STRING("LEFT_CENTER");
+ } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_LEFT_BOTTOM) {
+ attachPointParent = CLAY_STRING("LEFT_BOTTOM");
+ } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_CENTER_TOP) {
+ attachPointParent = CLAY_STRING("CENTER_TOP");
+ } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_CENTER_CENTER) {
+ attachPointParent = CLAY_STRING("CENTER_CENTER");
+ } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_CENTER_BOTTOM) {
+ attachPointParent = CLAY_STRING("CENTER_BOTTOM");
+ } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_RIGHT_TOP) {
+ attachPointParent = CLAY_STRING("RIGHT_TOP");
+ } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_RIGHT_CENTER) {
+ attachPointParent = CLAY_STRING("RIGHT_CENTER");
+ } else if (floatingConfig->attachPoints.parent == CLAY_ATTACH_POINT_RIGHT_BOTTOM) {
+ attachPointParent = CLAY_STRING("RIGHT_BOTTOM");
+ }
+ CLAY_TEXT(CLAY_STRING(", parent: "), infoTextConfig);
+ CLAY_TEXT(attachPointParent, infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+ }
+ // .pointerCaptureMode
+ CLAY_TEXT(CLAY_STRING("Pointer Capture Mode"), infoTitleConfig);
+ Clay_String pointerCaptureMode = CLAY_STRING("NONE");
+ if (floatingConfig->pointerCaptureMode == CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH) {
+ pointerCaptureMode = CLAY_STRING("PASSTHROUGH");
+ }
+ CLAY_TEXT(pointerCaptureMode, infoTextConfig);
+ // .attachTo
+ CLAY_TEXT(CLAY_STRING("Attach To"), infoTitleConfig);
+ Clay_String attachTo = CLAY_STRING("NONE");
+ if (floatingConfig->attachTo == CLAY_ATTACH_TO_PARENT) {
+ attachTo = CLAY_STRING("PARENT");
+ } else if (floatingConfig->attachTo == CLAY_ATTACH_TO_ELEMENT_WITH_ID) {
+ attachTo = CLAY_STRING("ELEMENT_WITH_ID");
+ } else if (floatingConfig->attachTo == CLAY_ATTACH_TO_ROOT) {
+ attachTo = CLAY_STRING("ROOT");
+ }
+ CLAY_TEXT(attachTo, infoTextConfig);
+ // .clipTo
+ CLAY_TEXT(CLAY_STRING("Clip To"), infoTitleConfig);
+ Clay_String clipTo = CLAY_STRING("ATTACHED_PARENT");
+ if (floatingConfig->clipTo == CLAY_CLIP_TO_NONE) {
+ clipTo = CLAY_STRING("NONE");
+ }
+ CLAY_TEXT(clipTo, infoTextConfig);
+ }
+ }
+ Clay_ClipElementConfig *clipConfig = &selectedItem->layoutElement->config.clip;
+ if (clipConfig->horizontal || clipConfig->vertical) {
+ CLAY_AUTO_ID({ .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+ Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_CLIP);
+ // .vertical
+ CLAY_TEXT(CLAY_STRING("Vertical"), infoTitleConfig);
+ CLAY_TEXT(clipConfig->vertical ? CLAY_STRING("true") : CLAY_STRING("false") , infoTextConfig);
+ // .horizontal
+ CLAY_TEXT(CLAY_STRING("Horizontal"), infoTitleConfig);
+ CLAY_TEXT(clipConfig->horizontal ? CLAY_STRING("true") : CLAY_STRING("false") , infoTextConfig);
+ }
+ }
+ Clay_BorderElementConfig *borderConfig = &selectedItem->layoutElement->config.border;
+ if (Clay__BorderHasAnyWidth(borderConfig)) {
+ CLAY(CLAY_ID("Clay__DebugViewElementInfoBorderBody"), { .layout = { .padding = attributeConfigPadding, .childGap = 8, .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
+ Clay__DebugViewRenderElementConfigHeader(selectedItem->elementId.stringId, CLAY__ELEMENT_CONFIG_TYPE_BORDER);
+ CLAY_TEXT(CLAY_STRING("Border Widths"), infoTitleConfig);
+ CLAY_AUTO_ID({ .layout = { .layoutDirection = CLAY_LEFT_TO_RIGHT } }) {
+ CLAY_TEXT(CLAY_STRING("{ left: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(borderConfig->width.left), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", right: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(borderConfig->width.right), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", top: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(borderConfig->width.top), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(", bottom: "), infoTextConfig);
+ CLAY_TEXT(Clay__IntToString(borderConfig->width.bottom), infoTextConfig);
+ CLAY_TEXT(CLAY_STRING(" }"), infoTextConfig);
+ }
+ // .textColor
+ CLAY_TEXT(CLAY_STRING("Border Color"), infoTitleConfig);
+ Clay__RenderDebugViewColor(borderConfig->color, infoTextConfig);
+ }
+ }
+ }
+ }
+ } else {
+ CLAY(CLAY_ID("Clay__DebugViewWarningsScrollPane"), { .layout = { .sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(300)}, .childGap = 6, .layoutDirection = CLAY_TOP_TO_BOTTOM }, .backgroundColor = CLAY__DEBUGVIEW_COLOR_2, .clip = { .horizontal = true, .vertical = true, .childOffset = Clay_GetScrollOffset() } }) {
+ Clay_TextElementConfig warningConfig = CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_4, .fontSize = 16, .wrapMode = CLAY_TEXT_WRAP_NONE });
+ CLAY(CLAY_ID("Clay__DebugViewWarningItemHeader"), { .layout = { .sizing = {.height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childGap = 8, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+ CLAY_TEXT(CLAY_STRING("Warnings"), warningConfig);
+ }
+ CLAY(CLAY_ID("Clay__DebugViewWarningsTopBorder"), { .layout = { .sizing = { .width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(1)} }, .backgroundColor = {200, 200, 200, 255} }) {}
+ int32_t previousWarningsLength = context->warnings.length;
+ for (int32_t i = 0; i < previousWarningsLength; i++) {
+ Clay__Warning warning = context->warnings.internalArray[i];
+ CLAY(CLAY_IDI("Clay__DebugViewWarningItem", i), { .layout = { .sizing = {.height = CLAY_SIZING_FIXED(CLAY__DEBUGVIEW_ROW_HEIGHT)}, .padding = {CLAY__DEBUGVIEW_OUTER_PADDING, CLAY__DEBUGVIEW_OUTER_PADDING, 0, 0 }, .childGap = 8, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} } }) {
+ CLAY_TEXT(warning.baseMessage, warningConfig);
+ if (warning.dynamicMessage.length > 0) {
+ CLAY_TEXT(warning.dynamicMessage, warningConfig);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+#pragma endregion
+
+uint32_t Clay__debugViewWidth = 400;
+Clay_Color Clay__debugViewHighlightColor = { 168, 66, 28, 100 };
+
+Clay__WarningArray Clay__WarningArray_Allocate_Arena(int32_t capacity, Clay_Arena *arena) {
+ size_t totalSizeBytes = capacity * sizeof(Clay_String);
+ Clay__WarningArray array = {.capacity = capacity, .length = 0};
+ uintptr_t nextAllocOffset = arena->nextAllocation + (64 - (arena->nextAllocation % 64));
+ if (nextAllocOffset + totalSizeBytes <= arena->capacity) {
+ array.internalArray = (Clay__Warning*)((uintptr_t)arena->memory + (uintptr_t)nextAllocOffset);
+ arena->nextAllocation = nextAllocOffset + totalSizeBytes;
+ }
+ else {
+ Clay__currentContext->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED,
+ .errorText = CLAY_STRING("Clay attempted to allocate memory in its arena, but ran out of capacity. Try increasing the capacity of the arena passed to Clay_Initialize()"),
+ .userData = Clay__currentContext->errorHandler.userData });
+ }
+ return array;
+}
+
+Clay__Warning *Clay__WarningArray_Add(Clay__WarningArray *array, Clay__Warning item)
+{
+ if (array->length < array->capacity) {
+ array->internalArray[array->length++] = item;
+ return &array->internalArray[array->length - 1];
+ }
+ return &CLAY__WARNING_DEFAULT;
+}
+
+void* Clay__Array_Allocate_Arena(int32_t capacity, uint32_t itemSize, Clay_Arena *arena)
+{
+ size_t totalSizeBytes = capacity * itemSize;
+ uintptr_t nextAllocOffset = arena->nextAllocation + ((64 - (arena->nextAllocation % 64)) & 63);
+ if (nextAllocOffset + totalSizeBytes <= arena->capacity) {
+ arena->nextAllocation = nextAllocOffset + totalSizeBytes;
+ return (void*)((uintptr_t)arena->memory + (uintptr_t)nextAllocOffset);
+ }
+ else {
+ Clay__currentContext->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_ARENA_CAPACITY_EXCEEDED,
+ .errorText = CLAY_STRING("Clay attempted to allocate memory in its arena, but ran out of capacity. Try increasing the capacity of the arena passed to Clay_Initialize()"),
+ .userData = Clay__currentContext->errorHandler.userData });
+ }
+ return CLAY__NULL;
+}
+
+bool Clay__Array_RangeCheck(int32_t index, int32_t length)
+{
+ if (index < length && index >= 0) {
+ return true;
+ }
+ Clay_Context* context = Clay_GetCurrentContext();
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_INTERNAL_ERROR,
+ .errorText = CLAY_STRING("Clay attempted to make an out of bounds array access. This is an internal error and is likely a bug."),
+ .userData = context->errorHandler.userData });
+ return false;
+}
+
+bool Clay__Array_AddCapacityCheck(int32_t length, int32_t capacity)
+{
+ if (length < capacity) {
+ return true;
+ }
+ Clay_Context* context = Clay_GetCurrentContext();
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_INTERNAL_ERROR,
+ .errorText = CLAY_STRING("Clay attempted to make an out of bounds array access. This is an internal error and is likely a bug."),
+ .userData = context->errorHandler.userData });
+ return false;
+}
+
+// PUBLIC API FROM HERE ---------------------------------------
+
+CLAY_WASM_EXPORT("Clay_MinMemorySize")
+uint32_t Clay_MinMemorySize(void) {
+ Clay_Context fakeContext = {
+ .maxElementCount = Clay__defaultMaxElementCount,
+ .maxMeasureTextCacheWordCount = Clay__defaultMaxMeasureTextWordCacheCount,
+ .internalArena = {
+ .capacity = SIZE_MAX,
+ .memory = NULL,
+ }
+ };
+ Clay_Context* currentContext = Clay_GetCurrentContext();
+ if (currentContext) {
+ fakeContext.maxElementCount = currentContext->maxElementCount;
+ fakeContext.maxMeasureTextCacheWordCount = currentContext->maxMeasureTextCacheWordCount;
+ }
+ // Reserve space in the arena for the context, important for calculating min memory size correctly
+ Clay__Context_Allocate_Arena(&fakeContext.internalArena);
+ Clay__InitializePersistentMemory(&fakeContext);
+ Clay__InitializeEphemeralMemory(&fakeContext);
+ return (uint32_t)fakeContext.internalArena.nextAllocation + 128;
+}
+
+CLAY_WASM_EXPORT("Clay_CreateArenaWithCapacityAndMemory")
+Clay_Arena Clay_CreateArenaWithCapacityAndMemory(size_t capacity, void *memory) {
+ Clay_Arena arena = {
+ .capacity = capacity,
+ .memory = (char *)memory
+ };
+ return arena;
+}
+
+#ifndef CLAY_WASM
+void Clay_SetMeasureTextFunction(Clay_Dimensions (*measureTextFunction)(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData), void *userData) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ Clay__MeasureText = measureTextFunction;
+ context->measureTextUserData = userData;
+}
+void Clay_SetQueryScrollOffsetFunction(Clay_Vector2 (*queryScrollOffsetFunction)(uint32_t elementId, void *userData), void *userData) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ Clay__QueryScrollOffset = queryScrollOffsetFunction;
+ context->queryScrollOffsetUserData = userData;
+}
+#endif
+
+CLAY_WASM_EXPORT("Clay_SetLayoutDimensions")
+void Clay_SetLayoutDimensions(Clay_Dimensions dimensions) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ context->rootResizedLastFrame = !Clay__FloatEqual(context->layoutDimensions.width, dimensions.width) || !Clay__FloatEqual(context->layoutDimensions.height, dimensions.height);
+ context->layoutDimensions = dimensions;
+}
+
+CLAY_WASM_EXPORT("Clay_SetLayoutDimensions")
+Clay_Dimensions Clay_GetLayoutDimensions() {
+ Clay_Context* context = Clay_GetCurrentContext();
+ return context->layoutDimensions;
+}
+
+CLAY_WASM_EXPORT("Clay_SetPointerState")
+void Clay_SetPointerState(Clay_Vector2 position, bool isPointerDown) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->booleanWarnings.maxElementsExceeded) {
+ return;
+ }
+ context->pointerInfo.position = position;
+ context->pointerOverIds.length = 0;
+ Clay__int32_tArray dfsBuffer = context->layoutElementChildrenBuffer;
+ for (int32_t rootIndex = context->layoutElementTreeRoots.length - 1; rootIndex >= 0; --rootIndex) {
+ dfsBuffer.length = 0;
+ Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&context->layoutElementTreeRoots, rootIndex);
+ Clay__int32_tArray_Add(&dfsBuffer, (int32_t)root->layoutElementIndex);
+ context->treeNodeVisited.internalArray[0] = false;
+ bool found = false;
+ bool skipTree = false;
+ while (dfsBuffer.length > 0) {
+ if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
+ dfsBuffer.length--;
+ continue;
+ }
+ context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
+ Clay_LayoutElement *currentElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&dfsBuffer, (int)dfsBuffer.length - 1));
+
+ Clay_LayoutElementHashMapItem *mapItem = Clay__GetHashMapItem(currentElement->id); // TODO think of a way around this, maybe the fact that it's essentially a binary tree limits the cost, but the worst case is not great
+ int32_t clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, (int32_t)(currentElement - context->layoutElements.internalArray));
+ Clay_LayoutElementHashMapItem *clipItem = Clay__GetHashMapItem(clipElementId);
+ // This check skips mouse interactions for elements that are currently "exit transitioning"
+ if (mapItem && mapItem->generation > context->generation) {
+ // Conditionally skip mouse interactions on non-exit transitions, based on user config
+ if (!currentElement->isTextElement && currentElement->config.transition.handler) {
+ for (int I = 0; I < context->transitionDatas.length; ++I) {
+ Clay__TransitionDataInternal* data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, I);
+ if (data->elementId == currentElement->id) {
+ if (currentElement->config.transition.interactionHandling == CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION) {
+ if (data->state == CLAY_TRANSITION_STATE_EXITING || data->state == CLAY_TRANSITION_STATE_ENTERING || ((data->activeProperties & CLAY_TRANSITION_PROPERTY_POSITION) && data->state == CLAY_TRANSITION_STATE_TRANSITIONING)) {
+ skipTree = true;
+ }
+ } else if (currentElement->config.transition.interactionHandling == CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION) {
+ if (data->state == CLAY_TRANSITION_STATE_EXITING) {
+ skipTree = true;
+ }
+ }
+ }
+ }
+ }
+
+ if (skipTree) {
+ dfsBuffer.length--;
+ continue;
+ }
+
+ Clay_BoundingBox elementBox = mapItem->boundingBox;
+ elementBox.x -= root->pointerOffset.x;
+ elementBox.y -= root->pointerOffset.y;
+ if ((Clay__PointIsInsideRect(position, elementBox)) && (clipElementId == 0 || (Clay__PointIsInsideRect(position, clipItem->boundingBox)) || context->externalScrollHandlingEnabled)) {
+ if (!skipTree) {
+ if (mapItem->onHoverFunction) {
+ mapItem->onHoverFunction(mapItem->elementId, context->pointerInfo, mapItem->hoverFunctionUserData);
+ }
+ Clay_ElementIdArray_Add(&context->pointerOverIds, mapItem->elementId);
+ }
+ found = true;
+ }
+
+ for (int32_t i = currentElement->children.length - 1; i >= 0; --i) {
+ Clay__int32_tArray_Add(&dfsBuffer, currentElement->children.elements[i]);
+ context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = false; // TODO needs to be ranged checked
+ }
+ } else {
+ dfsBuffer.length--;
+ }
+ }
+
+ Clay_LayoutElement *rootElement = Clay_LayoutElementArray_Get(&context->layoutElements, root->layoutElementIndex);
+ if (found && rootElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE && rootElement->config.floating.pointerCaptureMode == CLAY_POINTER_CAPTURE_MODE_CAPTURE) {
+ break;
+ }
+ }
+
+ if (isPointerDown) {
+ if (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
+ context->pointerInfo.state = CLAY_POINTER_DATA_PRESSED;
+ } else if (context->pointerInfo.state != CLAY_POINTER_DATA_PRESSED) {
+ context->pointerInfo.state = CLAY_POINTER_DATA_PRESSED_THIS_FRAME;
+ }
+ } else {
+ if (context->pointerInfo.state == CLAY_POINTER_DATA_RELEASED_THIS_FRAME) {
+ context->pointerInfo.state = CLAY_POINTER_DATA_RELEASED;
+ } else if (context->pointerInfo.state != CLAY_POINTER_DATA_RELEASED) {
+ context->pointerInfo.state = CLAY_POINTER_DATA_RELEASED_THIS_FRAME;
+ }
+ }
+}
+
+CLAY_WASM_EXPORT("Clay_GetPointerState")
+CLAY_DLL_EXPORT Clay_PointerData Clay_GetPointerState(void) {
+ return Clay_GetCurrentContext()->pointerInfo;
+}
+
+CLAY_WASM_EXPORT("Clay_Initialize")
+Clay_Context* Clay_Initialize(Clay_Arena arena, Clay_Dimensions layoutDimensions, Clay_ErrorHandler errorHandler) {
+ // Cacheline align memory passed in
+ uintptr_t baseOffset = 64 - ((uintptr_t)arena.memory % 64);
+ baseOffset = baseOffset == 64 ? 0 : baseOffset;
+ arena.memory += baseOffset;
+ Clay_Context *context = Clay__Context_Allocate_Arena(&arena);
+ if (context == NULL) return NULL;
+ // DEFAULTS
+ Clay_Context *oldContext = Clay_GetCurrentContext();
+ *context = CLAY__INIT(Clay_Context) {
+ .maxElementCount = oldContext ? oldContext->maxElementCount : Clay__defaultMaxElementCount,
+ .maxMeasureTextCacheWordCount = oldContext ? oldContext->maxMeasureTextCacheWordCount : Clay__defaultMaxMeasureTextWordCacheCount,
+ .errorHandler = errorHandler.errorHandlerFunction ? errorHandler : CLAY__INIT(Clay_ErrorHandler) { Clay__ErrorHandlerFunctionDefault, 0 },
+ .layoutDimensions = layoutDimensions,
+ .internalArena = arena,
+ };
+ Clay_SetCurrentContext(context);
+ Clay__InitializePersistentMemory(context);
+ Clay__InitializeEphemeralMemory(context);
+ for (int32_t i = 0; i < context->layoutElementsHashMap.capacity; ++i) {
+ context->layoutElementsHashMap.internalArray[i] = -1;
+ }
+ for (int32_t i = 0; i < context->measureTextHashMap.capacity; ++i) {
+ context->measureTextHashMap.internalArray[i] = 0;
+ }
+ context->measureTextHashMapInternal.length = 1; // Reserve the 0 value to mean "no next element"
+ context->layoutDimensions = layoutDimensions;
+ return context;
+}
+
+CLAY_WASM_EXPORT("Clay_GetCurrentContext")
+Clay_Context* Clay_GetCurrentContext(void) {
+ return Clay__currentContext;
+}
+
+CLAY_WASM_EXPORT("Clay_SetCurrentContext")
+void Clay_SetCurrentContext(Clay_Context* context) {
+ Clay__currentContext = context;
+}
+
+CLAY_WASM_EXPORT("Clay_GetScrollOffset")
+Clay_Vector2 Clay_GetScrollOffset(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->booleanWarnings.maxElementsExceeded) {
+ return CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT;
+ }
+ Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+ for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+ Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+ if (mapping->elementId == openLayoutElement->id) {
+ return mapping->scrollPosition;
+ }
+ }
+ return CLAY__INIT(Clay_Vector2) CLAY__DEFAULT_STRUCT;
+}
+
+CLAY_WASM_EXPORT("Clay_UpdateScrollContainers")
+void Clay_UpdateScrollContainers(bool enableDragScrolling, Clay_Vector2 scrollDelta, float deltaTime) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ bool isPointerActive = enableDragScrolling && (context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED || context->pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME);
+ // Don't apply scroll events to ancestors of the inner element
+ int32_t highestPriorityElementIndex = -1;
+ Clay__ScrollContainerDataInternal *highestPriorityScrollData = CLAY__NULL;
+ for (int32_t i = 0; i < context->scrollContainerDatas.length; i++) {
+ Clay__ScrollContainerDataInternal *scrollData = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+ if (!scrollData->openThisFrame) {
+ Clay__ScrollContainerDataInternalArray_RemoveSwapback(&context->scrollContainerDatas, i);
+ continue;
+ }
+ scrollData->openThisFrame = false;
+ Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(scrollData->elementId);
+ // Element isn't rendered this frame but scroll offset has been retained
+ if (!hashMapItem) {
+ Clay__ScrollContainerDataInternalArray_RemoveSwapback(&context->scrollContainerDatas, i);
+ continue;
+ }
+
+ // Touch / click is released
+ if (!isPointerActive && scrollData->pointerScrollActive) {
+ float xDiff = scrollData->scrollPosition.x - scrollData->scrollOrigin.x;
+ if (xDiff < -10 || xDiff > 10) {
+ scrollData->scrollMomentum.x = (scrollData->scrollPosition.x - scrollData->scrollOrigin.x) / (scrollData->momentumTime * 25);
+ }
+ float yDiff = scrollData->scrollPosition.y - scrollData->scrollOrigin.y;
+ if (yDiff < -10 || yDiff > 10) {
+ scrollData->scrollMomentum.y = (scrollData->scrollPosition.y - scrollData->scrollOrigin.y) / (scrollData->momentumTime * 25);
+ }
+ scrollData->pointerScrollActive = false;
+
+ scrollData->pointerOrigin = CLAY__INIT(Clay_Vector2){0,0};
+ scrollData->scrollOrigin = CLAY__INIT(Clay_Vector2){0,0};
+ scrollData->momentumTime = 0;
+ }
+
+ // Apply existing momentum
+ scrollData->scrollPosition.x += scrollData->scrollMomentum.x;
+ scrollData->scrollMomentum.x *= 0.95f;
+ bool scrollOccurred = scrollDelta.x != 0 || scrollDelta.y != 0;
+ if ((scrollData->scrollMomentum.x > -0.1f && scrollData->scrollMomentum.x < 0.1f) || scrollOccurred) {
+ scrollData->scrollMomentum.x = 0;
+ }
+ scrollData->scrollPosition.x = CLAY__MIN(CLAY__MAX(scrollData->scrollPosition.x, -(CLAY__MAX(scrollData->contentSize.width - scrollData->layoutElement->dimensions.width, 0))), 0);
+
+ scrollData->scrollPosition.y += scrollData->scrollMomentum.y;
+ scrollData->scrollMomentum.y *= 0.95f;
+ if ((scrollData->scrollMomentum.y > -0.1f && scrollData->scrollMomentum.y < 0.1f) || scrollOccurred) {
+ scrollData->scrollMomentum.y = 0;
+ }
+ scrollData->scrollPosition.y = CLAY__MIN(CLAY__MAX(scrollData->scrollPosition.y, -(CLAY__MAX(scrollData->contentSize.height - scrollData->layoutElement->dimensions.height, 0))), 0);
+
+ for (int32_t j = 0; j < context->pointerOverIds.length; ++j) { // TODO n & m are small here but this being n*m gives me the creeps
+ if (scrollData->layoutElement->id == Clay_ElementIdArray_Get(&context->pointerOverIds, j)->id) {
+ highestPriorityElementIndex = j;
+ highestPriorityScrollData = scrollData;
+ }
+ }
+ }
+
+ if (highestPriorityElementIndex > -1 && highestPriorityScrollData) {
+ Clay_LayoutElement *scrollElement = highestPriorityScrollData->layoutElement;
+ Clay_ClipElementConfig *clipConfig = &scrollElement->config.clip;
+ bool canScrollVertically = clipConfig->vertical && highestPriorityScrollData->contentSize.height > scrollElement->dimensions.height;
+ bool canScrollHorizontally = clipConfig->horizontal && highestPriorityScrollData->contentSize.width > scrollElement->dimensions.width;
+ // Handle wheel scroll
+ if (canScrollVertically) {
+ highestPriorityScrollData->scrollPosition.y = highestPriorityScrollData->scrollPosition.y + scrollDelta.y * 10;
+ }
+ if (canScrollHorizontally) {
+ highestPriorityScrollData->scrollPosition.x = highestPriorityScrollData->scrollPosition.x + scrollDelta.x * 10;
+ }
+ // Handle click / touch scroll
+ if (isPointerActive) {
+ highestPriorityScrollData->scrollMomentum = CLAY__INIT(Clay_Vector2)CLAY__DEFAULT_STRUCT;
+ if (!highestPriorityScrollData->pointerScrollActive) {
+ highestPriorityScrollData->pointerOrigin = context->pointerInfo.position;
+ highestPriorityScrollData->scrollOrigin = highestPriorityScrollData->scrollPosition;
+ highestPriorityScrollData->pointerScrollActive = true;
+ } else {
+ float scrollDeltaX = 0, scrollDeltaY = 0;
+ if (canScrollHorizontally) {
+ float oldXScrollPosition = highestPriorityScrollData->scrollPosition.x;
+ highestPriorityScrollData->scrollPosition.x = highestPriorityScrollData->scrollOrigin.x + (context->pointerInfo.position.x - highestPriorityScrollData->pointerOrigin.x);
+ highestPriorityScrollData->scrollPosition.x = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.x, 0), -(highestPriorityScrollData->contentSize.width - highestPriorityScrollData->boundingBox.width));
+ scrollDeltaX = highestPriorityScrollData->scrollPosition.x - oldXScrollPosition;
+ }
+ if (canScrollVertically) {
+ float oldYScrollPosition = highestPriorityScrollData->scrollPosition.y;
+ highestPriorityScrollData->scrollPosition.y = highestPriorityScrollData->scrollOrigin.y + (context->pointerInfo.position.y - highestPriorityScrollData->pointerOrigin.y);
+ highestPriorityScrollData->scrollPosition.y = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.y, 0), -(highestPriorityScrollData->contentSize.height - highestPriorityScrollData->boundingBox.height));
+ scrollDeltaY = highestPriorityScrollData->scrollPosition.y - oldYScrollPosition;
+ }
+ if (scrollDeltaX > -0.1f && scrollDeltaX < 0.1f && scrollDeltaY > -0.1f && scrollDeltaY < 0.1f && highestPriorityScrollData->momentumTime > 0.15f) {
+ highestPriorityScrollData->momentumTime = 0;
+ highestPriorityScrollData->pointerOrigin = context->pointerInfo.position;
+ highestPriorityScrollData->scrollOrigin = highestPriorityScrollData->scrollPosition;
+ } else {
+ highestPriorityScrollData->momentumTime += deltaTime;
+ }
+ }
+ }
+ // Clamp any changes to scroll position to the maximum size of the contents
+ if (canScrollVertically) {
+ highestPriorityScrollData->scrollPosition.y = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.y, 0), -(highestPriorityScrollData->contentSize.height - scrollElement->dimensions.height));
+ }
+ if (canScrollHorizontally) {
+ highestPriorityScrollData->scrollPosition.x = CLAY__MAX(CLAY__MIN(highestPriorityScrollData->scrollPosition.x, 0), -(highestPriorityScrollData->contentSize.width - scrollElement->dimensions.width));
+ }
+ }
+}
+
+CLAY_WASM_EXPORT("Clay_BeginLayout")
+void Clay_BeginLayout(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ Clay__InitializeEphemeralMemory(context);
+ context->generation++;
+ context->dynamicElementIndex = 0;
+ // Set up the root container that covers the entire window
+ Clay_Dimensions rootDimensions = {context->layoutDimensions.width, context->layoutDimensions.height};
+ if (context->debugModeEnabled) {
+ rootDimensions.width -= (float)Clay__debugViewWidth;
+ }
+ context->booleanWarnings = CLAY__INIT(Clay_BooleanWarnings) CLAY__DEFAULT_STRUCT;
+ Clay__OpenElementWithId(CLAY_ID("Clay__RootContainer"));
+ Clay__ConfigureOpenElement(CLAY__INIT(Clay_ElementDeclaration) {
+ .layout = { .sizing = {CLAY_SIZING_FIXED((rootDimensions.width)), CLAY_SIZING_FIXED(rootDimensions.height)} }
+ });
+ Clay__int32_tArray_Add(&context->openLayoutElementStack, 0);
+ Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) { .layoutElementIndex = 0 });
+}
+
+void Clay__CloneElementsWithExitTransition() {
+ Clay_Context* context = Clay_GetCurrentContext();
+ int32_t nextIndex = context->layoutElements.capacity - 1;
+ int32_t nextChildIndex = context->layoutElementChildren.capacity - 1;
+
+ for (int i = 0; i < context->transitionDatas.length; ++i) {
+ Clay__TransitionDataInternal *data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+ Clay_TransitionElementConfig* config = &data->elementThisFrame->config.transition;
+ if (data->transitionOut) {
+ Clay__int32_tArray bfsBuffer = context->openLayoutElementStack;
+ bfsBuffer.length = 0;
+ Clay_LayoutElement* newElement = Clay_LayoutElementArray_Set_DontTouchLength(&context->layoutElements, nextIndex, *data->elementThisFrame);
+ Clay__StringArray_Set_DontTouchLength(&context->layoutElementIdStrings, nextIndex, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, data->elementThisFrame - context->layoutElements.internalArray));
+ Clay__int32_tArray_Add(&bfsBuffer, nextIndex);
+ data->elementThisFrame = newElement;
+ nextIndex--;
+
+ int32_t bufferIndex = 0;
+ while(bufferIndex < bfsBuffer.length) {
+ Clay_LayoutElement *layoutElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, Clay__int32_tArray_GetValue(&bfsBuffer, bufferIndex));
+ bufferIndex++;
+ for (int j = layoutElement->children.length - 1; j >= 0; --j) {
+ Clay_LayoutElement* childElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, layoutElement->children.elements[j]);
+ Clay__int32_tArray_Add(&bfsBuffer, nextIndex);
+ Clay_LayoutElement* newChildElement = Clay_LayoutElementArray_Set_DontTouchLength(&context->layoutElements, nextIndex, *childElement);
+ Clay__StringArray_Set_DontTouchLength(&context->layoutElementIdStrings, nextIndex, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, childElement - context->layoutElements.internalArray));
+ Clay__int32_tArray_Set_DontTouchLength(&context->layoutElementChildren, nextChildIndex, nextIndex);
+ nextIndex--;
+ nextChildIndex--;
+ }
+ layoutElement->children.elements = &context->layoutElementChildren.internalArray[nextChildIndex + 1];
+ }
+ }
+ }
+};
+
+void Clay_ApplyTransitionedPropertiesToElement(Clay_LayoutElement* currentElement, Clay_TransitionProperty properties, Clay_TransitionData currentTransitionData, Clay_BoundingBox* boundingBox, bool reparented) {
+ if (properties & CLAY_TRANSITION_PROPERTY_WIDTH) {
+ if (!reparented) {
+ currentElement->dimensions.width = currentTransitionData.boundingBox.width;
+ currentElement->config.layout.sizing.width = CLAY_SIZING_FIXED(currentTransitionData.boundingBox.width);
+ } else {
+ boundingBox->width = currentTransitionData.boundingBox.width;
+ }
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_HEIGHT) {
+ if (!reparented) {
+ currentElement->dimensions.height = currentTransitionData.boundingBox.height;
+ currentElement->config.layout.sizing.height = CLAY_SIZING_FIXED(currentTransitionData.boundingBox.height);
+ } else {
+ boundingBox->height = currentTransitionData.boundingBox.height;
+ }
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_X) {
+ boundingBox->x = currentTransitionData.boundingBox.x;
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_Y) {
+ boundingBox->y = currentTransitionData.boundingBox.y;
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) {
+ currentElement->config.overlayColor = currentTransitionData.overlayColor;
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) {
+ currentElement->config.backgroundColor = currentTransitionData.backgroundColor;
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) {
+ currentElement->config.border.color = currentTransitionData.borderColor;
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) {
+ currentElement->config.border.width = currentTransitionData.borderWidth;
+ }
+}
+
+CLAY_WASM_EXPORT("Clay_EndLayout")
+Clay_RenderCommandArray Clay_EndLayout(float deltaTime) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ Clay__CloseElement();
+
+ if (context->openLayoutElementStack.length > 1) {
+ context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) {
+ .errorType = CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE,
+ .errorText = CLAY_STRING("There were still open layout elements when EndLayout was called. This results from an unequal number of calls to Clay__OpenElement and Clay__CloseElement."),
+ .userData = context->errorHandler.userData });
+ }
+
+ // Prune non exiting transitions
+ for (int i = 0; i < context->transitionDatas.length; ++i) {
+ Clay__TransitionDataInternal *data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+ Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(data->elementId);
+ // Transition element exited and doesn't have an exit handler defined
+ // Or, the user deleted the transition handler from one frame to the next
+ if (!data->transitionOut && (hashMapItem->generation <= context->generation || !hashMapItem->layoutElement->config.transition.handler)) {
+ Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, i);
+ i--;
+ continue;
+ }
+ }
+
+ Clay__int32_tArray elementIdsToRemoveTransitions = context->reusableElementIndexBuffer;
+ elementIdsToRemoveTransitions.length = 0;
+
+ for (int i = 0; i < context->transitionDatas.length; ++i) {
+ Clay__TransitionDataInternal *data = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+ Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(data->elementId);
+ // This might seems strange - can't we just look up the element itself, and check the config to see whether it has an exit transition defined?
+ // That would work fine if the element actually had an exit transition in the first place. If it doesn't have an exit transition defined, the element
+ // will have simply disappeared completely at this point, and there will be no element through which to access the config.
+ if (data->transitionOut) {
+ Clay_TransitionElementConfig* config = &data->elementThisFrame->config.transition;
+ // Element wasn't found this frame - either delete transition data or transition out
+ if (hashMapItem->generation <= context->generation) {
+ Clay_LayoutElementHashMapItem *parentHashMapItem = Clay__GetHashMapItem(data->parentId);
+ // Don't exit transition if the parent has also exited and SKIP_WHEN_PARENT_EXITS is used
+ if (config->exit.trigger == CLAY_TRANSITION_EXIT_TRIGGER_WHEN_PARENT_EXITS || !parentHashMapItem || parentHashMapItem->generation > context->generation) {
+ // This if only runs one single time when the element first starts exiting
+ if (data->state != CLAY_TRANSITION_STATE_EXITING) {
+ if (parentHashMapItem->generation <= context->generation) {
+ data->elementThisFrame->config.floating.attachTo = CLAY_ATTACH_TO_ROOT;
+ data->elementThisFrame->config.floating.offset = CLAY__INIT(Clay_Vector2) { hashMapItem->boundingBox.x, hashMapItem->boundingBox.y };
+ data->elementThisFrame->config.floating.parentId = Clay__HashString(CLAY_STRING("Clay__RootContainer"), 0).id;
+ }
+ hashMapItem->appearedThisFrame = false;
+ data->elementThisFrame->exiting = true;
+ data->elementThisFrame->config.layout.sizing.width = CLAY_SIZING_FIXED(data->elementThisFrame->dimensions.width);
+ data->elementThisFrame->config.layout.sizing.height = CLAY_SIZING_FIXED(data->elementThisFrame->dimensions.height);
+ data->state = CLAY_TRANSITION_STATE_EXITING;
+ data->activeProperties = config->properties;
+ data->elapsedTime = 0;
+ data->targetState = config->exit.setFinalState(data->targetState, config->properties);
+ }
+
+ // Below this line runs every frame while element is exiting -----------
+
+ // Clone the entire subtree back into the main UI layout tree
+ Clay__int32_tArray bfsBuffer = context->openLayoutElementStack;
+ bfsBuffer.length = 0;
+ data->elementThisFrame = Clay_LayoutElementArray_Add(&context->layoutElements, *data->elementThisFrame);
+ int32_t exitingElementIndex = data->elementThisFrame - context->layoutElements.internalArray;
+ Clay__StringArray_Add(&context->layoutElementIdStrings, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, exitingElementIndex));
+ Clay__int32_tArray_Add(&context->layoutElementClipElementIds, *Clay__int32_tArray_GetCheckCapacity(&context->layoutElementClipElementIds, exitingElementIndex));
+ Clay__int32_tArray_Add(&bfsBuffer, exitingElementIndex);
+ int32_t bufferIndex = 0;
+ while (bufferIndex < bfsBuffer.length) {
+ Clay_LayoutElement *layoutElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, Clay__int32_tArray_GetValue(&bfsBuffer, bufferIndex));
+ Clay_LayoutElementHashMapItem* bfsMapItem = Clay__GetHashMapItem(layoutElement->id);
+ // Children of exiting elements may have been moved elsewhere in the layout, this prevents a duplicate ID error if they still exist.
+ if (bfsMapItem->generation <= context->generation) {
+ Clay__AddHashMapItem(CLAY__INIT(Clay_ElementId){ layoutElement->id }, layoutElement);
+ int32_t firstChildSlot = context->layoutElementChildren.length;
+ uint16_t newChildrenLength = layoutElement->children.length;
+ for (int j = 0; j < layoutElement->children.length; ++j) {
+ Clay_LayoutElement* childElement = Clay_LayoutElementArray_GetCheckCapacity(&context->layoutElements, layoutElement->children.elements[j]);
+ Clay_LayoutElementHashMapItem* childMapItem = Clay__GetHashMapItem(childElement->id);
+ if (childMapItem->generation <= context->generation) {
+ // Remove any nested transitions inside exiting trees
+ if (!childElement->isTextElement && childElement->config.transition.handler) {
+ Clay__int32_tArray_Add(&elementIdsToRemoveTransitions, childElement->id);
+ }
+ int32_t childElementIndex = childElement - context->layoutElements.internalArray;
+ Clay_LayoutElement* newChildElement = Clay_LayoutElementArray_Add(&context->layoutElements, *childElement);
+ Clay__StringArray_Add(&context->layoutElementIdStrings, *Clay__StringArray_GetCheckCapacity(&context->layoutElementIdStrings, childElementIndex));
+ Clay__int32_tArray_Add(&context->layoutElementClipElementIds, *Clay__int32_tArray_GetCheckCapacity(&context->layoutElementClipElementIds, childElementIndex));
+ Clay__int32_tArray_Add(&bfsBuffer, context->layoutElements.length - 1);
+ if (newChildElement->isTextElement) {
+ newChildElement->textElementData.wrappedLines.length = 0;
+ }
+ Clay__int32_tArray_Add(&context->layoutElementChildren, context->layoutElements.length - 1);
+ } else {
+ newChildrenLength--;
+ }
+ }
+ layoutElement->children = CLAY__INIT(Clay__LayoutElementChildren) {
+ .elements = &context->layoutElementChildren.internalArray[firstChildSlot],
+ .length = newChildrenLength,
+ };
+ }
+ bufferIndex++;
+ }
+ hashMapItem->layoutElement = data->elementThisFrame;
+
+ // Reattach the inserted subtree to its previous parent if it still exists
+ // and the exiting element is not floating
+ Clay_FloatingElementConfig* floatingConfig = &hashMapItem->layoutElement->config.floating;
+ if (parentHashMapItem->generation > context->generation && floatingConfig->attachTo == CLAY_ATTACH_TO_NONE) {
+ Clay_LayoutElement *parentElement = parentHashMapItem->layoutElement;
+ int32_t newChildrenStartIndex = context->layoutElementChildren.length;
+ bool found = false;
+ if (config->exit.siblingOrdering == CLAY_EXIT_TRANSITION_ORDERING_UNDERNEATH_SIBLINGS) {
+ Clay__int32_tArray_Add(&context->layoutElementChildren, exitingElementIndex);
+ found = true;
+ }
+ for (int j = 0; j < parentElement->children.length; ++j) {
+ if (config->exit.siblingOrdering == CLAY_EXIT_TRANSITION_ORDERING_NATURAL_ORDER && j == data->siblingIndex) {
+ Clay__int32_tArray_Add(&context->layoutElementChildren, exitingElementIndex);
+ found = true;
+ }
+ Clay__int32_tArray_Add(&context->layoutElementChildren, parentElement->children.elements[j]);
+ }
+ if (!found) {
+ Clay__int32_tArray_Add(&context->layoutElementChildren, exitingElementIndex);
+ }
+ parentElement->children.length++;
+ parentElement->children.elements = &context->layoutElementChildren.internalArray[newChildrenStartIndex];
+ // Otherwise, create the tree root for the floating element (needs to be created every frame)
+ } else {
+ Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) {
+ .layoutElementIndex = (int32_t)(data->elementThisFrame - context->layoutElements.internalArray),
+ .parentId = floatingConfig->parentId,
+ .zIndex = floatingConfig->zIndex,
+ });
+ }
+ // Parent exited, just delete child without exit transition
+ } else {
+ Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, i);
+ i--;
+ continue;
+ }
+ }
+ }
+ }
+
+ for (int i = 0; i < elementIdsToRemoveTransitions.length; ++i) {
+ for (int j = 0; j < context->transitionDatas.length; ++j) {
+ if (Clay__TransitionDataInternalArray_Get(&context->transitionDatas, j)->elementId == Clay__int32_tArray_GetValue(&elementIdsToRemoveTransitions, i)) {
+ Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, j);
+ break;
+ }
+ }
+ }
+
+ if (context->booleanWarnings.maxElementsExceeded) {
+ Clay_String message;
+ message = CLAY_STRING("Clay Error: Layout elements exceeded Clay__maxElementCount");
+ Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand ) {
+ .boundingBox = { context->layoutDimensions.width / 2 - 59 * 4, context->layoutDimensions.height / 2, 0, 0 },
+ .renderData = { .text = { .stringContents = CLAY__INIT(Clay_StringSlice) { .length = message.length, .chars = message.chars, .baseChars = message.chars }, .textColor = {255, 0, 0, 255}, .fontSize = 16 } },
+ .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT
+ });
+ } else {
+ if (context->transitionDatas.length > 0) {
+ Clay__CalculateFinalLayout(deltaTime, false, false);
+
+ for (int i = 0; i < context->transitionDatas.length; ++i) {
+ Clay__TransitionDataInternal* transitionData = Clay__TransitionDataInternalArray_Get(&context->transitionDatas, i);
+ Clay_LayoutElement* currentElement = transitionData->elementThisFrame;
+ Clay_LayoutElementHashMapItem* mapItem = Clay__GetHashMapItem(transitionData->elementId);
+ Clay_LayoutElementHashMapItem* parentMapItem = Clay__GetHashMapItem(transitionData->parentId);
+ Clay_TransitionData targetState = transitionData->targetState;
+ if (transitionData->state != CLAY_TRANSITION_STATE_EXITING) {
+ targetState = CLAY__INIT(Clay_TransitionData) {
+ mapItem->boundingBox,
+ currentElement->config.backgroundColor,
+ currentElement->config.overlayColor,
+ currentElement->config.border.color,
+ currentElement->config.border.width,
+ };
+ }
+ Clay_TransitionData oldTargetState = transitionData->targetState;
+ transitionData->targetState = targetState;
+ if (mapItem->appearedThisFrame) {
+ if (currentElement->config.transition.enter.setInitialState && !(parentMapItem->appearedThisFrame && currentElement->config.transition.enter.trigger == CLAY_TRANSITION_ENTER_SKIP_ON_FIRST_PARENT_FRAME)) {
+ transitionData->state = CLAY_TRANSITION_STATE_ENTERING;
+ transitionData->initialState = currentElement->config.transition.enter.setInitialState(transitionData->targetState, currentElement->config.transition.properties);
+ transitionData->currentState = transitionData->initialState;
+ transitionData->activeProperties = currentElement->config.transition.properties;
+ Clay_ApplyTransitionedPropertiesToElement(currentElement, currentElement->config.transition.properties, transitionData->initialState, &mapItem->boundingBox, transitionData->reparented);
+ } else {
+ transitionData->initialState = targetState;
+ transitionData->currentState = targetState;
+ transitionData->activeProperties = CLAY_TRANSITION_PROPERTY_NONE;
+ }
+ } else {
+ if (transitionData->state != CLAY_TRANSITION_STATE_EXITING) {
+ Clay_Vector2 parentScrollOffset = parentMapItem->layoutElement->config.clip.childOffset;
+ Clay_Vector2 newRelativePosition = {
+ mapItem->boundingBox.x - parentMapItem->boundingBox.x - parentScrollOffset.x,
+ mapItem->boundingBox.y - parentMapItem->boundingBox.y - parentScrollOffset.y,
+ };
+ Clay_Vector2 oldRelativePosition = transitionData->oldParentRelativePosition;
+ transitionData->oldParentRelativePosition = newRelativePosition;
+ Clay_TransitionProperty properties = currentElement->config.transition.properties;
+ int32_t newActiveProperties = CLAY_TRANSITION_PROPERTY_NONE;
+ if (properties & CLAY_TRANSITION_PROPERTY_X) {
+ // Don't trigger a transition if...
+ if (
+ // The element's absolute position didn't change
+ !Clay__FloatEqual(oldTargetState.boundingBox.x, targetState.boundingBox.x)
+ // The element is still in the same parent container, and it's parent-relative position didn't change (parent moved)
+ && (!(Clay__FloatEqual(oldRelativePosition.x, newRelativePosition.x)) || transitionData->reparented)
+ // The position changed was triggered by the outer window resizing
+ && !context->rootResizedLastFrame
+ ) {
+ newActiveProperties |= CLAY_TRANSITION_PROPERTY_X;
+ }
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_Y) {
+ // See extended comments above in PROPERTY_X for explanation
+ if (!Clay__FloatEqual(oldTargetState.boundingBox.y, targetState.boundingBox.y) && (!(Clay__FloatEqual(oldRelativePosition.y, newRelativePosition.y)) || transitionData->reparented) && !context->rootResizedLastFrame) {
+ newActiveProperties |= CLAY_TRANSITION_PROPERTY_Y;
+ }
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_WIDTH) {
+ if (!Clay__FloatEqual(oldTargetState.boundingBox.width, targetState.boundingBox.width) && !context->rootResizedLastFrame) {
+ newActiveProperties |= CLAY_TRANSITION_PROPERTY_WIDTH;
+ }
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_HEIGHT) {
+ if (!Clay__FloatEqual(oldTargetState.boundingBox.height, targetState.boundingBox.height) && !context->rootResizedLastFrame) {
+ newActiveProperties |= CLAY_TRANSITION_PROPERTY_HEIGHT;
+ }
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) {
+ if (!Clay__MemCmp((char *) &oldTargetState.backgroundColor, (char *)&targetState.backgroundColor, sizeof(Clay_Color))) {
+ newActiveProperties |= CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR;
+ }
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) {
+ if (!Clay__MemCmp((char *) &oldTargetState.overlayColor, (char *)&targetState.overlayColor, sizeof(Clay_Color))) {
+ newActiveProperties |= CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR;
+ }
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) {
+ if (!Clay__MemCmp((char *) &oldTargetState.borderColor, (char *)&targetState.borderColor, sizeof(Clay_Color))) {
+ newActiveProperties |= CLAY_TRANSITION_PROPERTY_BORDER_COLOR;
+ }
+ }
+ if (properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) {
+ if (!Clay__MemCmp((char *) &oldTargetState.borderWidth, (char *)&targetState.borderWidth, sizeof(Clay_BorderWidth))) {
+ newActiveProperties |= CLAY_TRANSITION_PROPERTY_BORDER_WIDTH;
+ }
+ }
+
+ if (newActiveProperties != 0) {
+ transitionData->elapsedTime = 0;
+ transitionData->initialState = transitionData->currentState;
+ transitionData->state = CLAY_TRANSITION_STATE_TRANSITIONING;
+ transitionData->activeProperties = (Clay_TransitionProperty)(transitionData->activeProperties | newActiveProperties);
+ }
+ }
+
+ if (transitionData->state == CLAY_TRANSITION_STATE_IDLE) {
+ transitionData->initialState = targetState;
+ transitionData->currentState = targetState;
+ transitionData->targetState = targetState;
+ transitionData->activeProperties = CLAY_TRANSITION_PROPERTY_NONE;
+ } else {
+ bool transitionComplete = true;
+ transitionComplete = currentElement->config.transition.handler(CLAY__INIT(Clay_TransitionCallbackArguments) {
+ transitionData->state,
+ transitionData->initialState,
+ &transitionData->currentState,
+ targetState,
+ transitionData->elapsedTime,
+ currentElement->config.transition.duration,
+ transitionData->activeProperties
+ });
+
+ Clay_ApplyTransitionedPropertiesToElement(currentElement, transitionData->activeProperties, transitionData->currentState, &mapItem->boundingBox, transitionData->reparented);
+ transitionData->elapsedTime += deltaTime;
+
+ if (transitionComplete) {
+ if (transitionData->state == CLAY_TRANSITION_STATE_ENTERING || transitionData->state == CLAY_TRANSITION_STATE_TRANSITIONING) {transitionData->state = CLAY_TRANSITION_STATE_IDLE;
+ transitionData->elapsedTime = 0;
+ transitionData->reparented = false;
+ transitionData->activeProperties = CLAY_TRANSITION_PROPERTY_NONE;
+ } else if (transitionData->state == CLAY_TRANSITION_STATE_EXITING) {
+ Clay__TransitionDataInternalArray_RemoveSwapback(&context->transitionDatas, i);
+ }
+ }
+ }
+ }
+ }
+
+ if (context->debugModeEnabled) {
+ context->warningsEnabled = false;
+ Clay__RenderDebugView();
+ context->warningsEnabled = true;
+ }
+
+ if (context->booleanWarnings.maxElementsExceeded) {
+ Clay_String message;
+ message = CLAY_STRING("Clay Error: Debug view caused layout element count to exceed Clay__maxElementCount");
+ Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand ) {
+ .boundingBox = { context->layoutDimensions.width / 2 - 59 * 4, context->layoutDimensions.height / 2, 0, 0 },
+ .renderData = { .text = { .stringContents = CLAY__INIT(Clay_StringSlice) { .length = message.length, .chars = message.chars, .baseChars = message.chars }, .textColor = {255, 0, 0, 255}, .fontSize = 16 } },
+ .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT
+ });
+ } else {
+ Clay__CalculateFinalLayout(deltaTime, true, true);
+ Clay__CloneElementsWithExitTransition();
+ }
+ } else {
+ if (context->debugModeEnabled) {
+ context->warningsEnabled = false;
+ Clay__RenderDebugView();
+ context->warningsEnabled = true;
+ }
+
+ if (context->booleanWarnings.maxElementsExceeded) {
+ Clay_String message;
+ message = CLAY_STRING("Clay Error: Debug view caused layout element count to exceed Clay__maxElementCount");
+ Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand ) {
+ .boundingBox = { context->layoutDimensions.width / 2 - 59 * 4, context->layoutDimensions.height / 2, 0, 0 },
+ .renderData = { .text = { .stringContents = CLAY__INIT(Clay_StringSlice) { .length = message.length, .chars = message.chars, .baseChars = message.chars }, .textColor = {255, 0, 0, 255}, .fontSize = 16 } },
+ .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT
+ });
+ } else {
+ Clay__CalculateFinalLayout(deltaTime, false, true);
+ }
+ }
+ }
+
+ for (int i = 0; i < context->layoutElementsHashMap.capacity; ++i) {
+ int32_t currentElementIndex = context->layoutElementsHashMap.internalArray[i];
+ int32_t previousElementIndex = -1;
+ while (currentElementIndex != -1) {
+ Clay_LayoutElementHashMapItem* currentItem = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, currentElementIndex);
+ int32_t nextIndex = currentItem->nextIndex;
+ // Needs to be pruned
+ if (currentItem->generation <= context->generation) {
+ // Delete the underlying item and add it to the freelist
+ Clay__LayoutElementHashMapItemArray_Set(&context->layoutElementsHashMapInternal, currentElementIndex, CLAY__INIT(Clay_LayoutElementHashMapItem) { .nextIndex = -1 });
+ Clay__int32_tArray_Add(&context->layoutElementsHashMapFreeList, currentElementIndex);
+ // If it's the very top of the bucket, rewrite the first bucket pointer
+ if (previousElementIndex == -1) {
+ Clay__int32_tArray_Set(&context->layoutElementsHashMap, i, nextIndex);
+ currentElementIndex = nextIndex;
+ previousElementIndex = -1;
+ } else {
+ // Rewrite previous pointer
+ Clay_LayoutElementHashMapItem* previousItem = Clay__LayoutElementHashMapItemArray_Get(&context->layoutElementsHashMapInternal, previousElementIndex);
+ previousItem->nextIndex = nextIndex;
+ currentElementIndex = nextIndex;
+ }
+ } else {
+ previousElementIndex = currentElementIndex;
+ currentElementIndex = nextIndex;
+ }
+ }
+ }
+
+ return context->renderCommands;
+}
+
+CLAY_WASM_EXPORT("Clay_GetOpenElementId")
+uint32_t Clay_GetOpenElementId(void) {
+ return Clay__GetOpenLayoutElement()->id;
+}
+
+CLAY_WASM_EXPORT("Clay_GetElementId")
+Clay_ElementId Clay_GetElementId(Clay_String idString) {
+ return Clay__HashString(idString, 0);
+}
+
+CLAY_WASM_EXPORT("Clay_GetElementIdWithIndex")
+Clay_ElementId Clay_GetElementIdWithIndex(Clay_String idString, uint32_t index) {
+ return Clay__HashStringWithOffset(idString, index, 0);
+}
+
+bool Clay_Hovered(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->booleanWarnings.maxElementsExceeded) {
+ return false;
+ }
+ Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+ for (int32_t i = 0; i < context->pointerOverIds.length; ++i) {
+ if (Clay_ElementIdArray_Get(&context->pointerOverIds, i)->id == openLayoutElement->id) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData), void *userData) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context->booleanWarnings.maxElementsExceeded) {
+ return;
+ }
+ Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement();
+ Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(openLayoutElement->id);
+ hashMapItem->onHoverFunction = onHoverFunction;
+ hashMapItem->hoverFunctionUserData = userData;
+}
+
+CLAY_WASM_EXPORT("Clay_PointerOver")
+bool Clay_PointerOver(Clay_ElementId elementId) { // TODO return priority for separating multiple results
+ Clay_Context* context = Clay_GetCurrentContext();
+ for (int32_t i = 0; i < context->pointerOverIds.length; ++i) {
+ if (Clay_ElementIdArray_Get(&context->pointerOverIds, i)->id == elementId.id) {
+ return true;
+ }
+ }
+ return false;
+}
+
+CLAY_WASM_EXPORT("Clay_GetScrollContainerData")
+Clay_ScrollContainerData Clay_GetScrollContainerData(Clay_ElementId id) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ for (int32_t i = 0; i < context->scrollContainerDatas.length; ++i) {
+ Clay__ScrollContainerDataInternal *scrollContainerData = Clay__ScrollContainerDataInternalArray_Get(&context->scrollContainerDatas, i);
+ if (scrollContainerData->elementId == id.id) {
+ if (!scrollContainerData->layoutElement) { // This can happen on the first frame before a scroll container is declared
+ return CLAY__INIT(Clay_ScrollContainerData) CLAY__DEFAULT_STRUCT;
+ }
+ return CLAY__INIT(Clay_ScrollContainerData) {
+ .scrollPosition = &scrollContainerData->scrollPosition,
+ .scrollContainerDimensions = { scrollContainerData->boundingBox.width, scrollContainerData->boundingBox.height },
+ .contentDimensions = scrollContainerData->contentSize,
+ .config = scrollContainerData->layoutElement->config.clip,
+ .found = true
+ };
+ }
+ }
+ return CLAY__INIT(Clay_ScrollContainerData) CLAY__DEFAULT_STRUCT;
+}
+
+CLAY_WASM_EXPORT("Clay_GetElementData")
+Clay_ElementData Clay_GetElementData(Clay_ElementId id){
+ Clay_LayoutElementHashMapItem * item = Clay__GetHashMapItem(id.id);
+ if(item == &Clay_LayoutElementHashMapItem_DEFAULT) {
+ return CLAY__INIT(Clay_ElementData) CLAY__DEFAULT_STRUCT;
+ }
+
+ return CLAY__INIT(Clay_ElementData){
+ .boundingBox = item->boundingBox,
+ .found = true
+ };
+}
+
+CLAY_WASM_EXPORT("Clay_SetDebugModeEnabled")
+void Clay_SetDebugModeEnabled(bool enabled) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ context->debugModeEnabled = enabled;
+}
+
+CLAY_WASM_EXPORT("Clay_IsDebugModeEnabled")
+bool Clay_IsDebugModeEnabled(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ return context->debugModeEnabled;
+}
+
+CLAY_WASM_EXPORT("Clay_SetCullingEnabled")
+void Clay_SetCullingEnabled(bool enabled) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ context->disableCulling = !enabled;
+}
+
+CLAY_WASM_EXPORT("Clay_SetExternalScrollHandlingEnabled")
+void Clay_SetExternalScrollHandlingEnabled(bool enabled) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ context->externalScrollHandlingEnabled = enabled;
+}
+
+CLAY_WASM_EXPORT("Clay_GetMaxElementCount")
+int32_t Clay_GetMaxElementCount(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ return context->maxElementCount;
+}
+
+CLAY_WASM_EXPORT("Clay_SetMaxElementCount")
+void Clay_SetMaxElementCount(int32_t maxElementCount) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context) {
+ context->maxElementCount = maxElementCount;
+ } else {
+ Clay__defaultMaxElementCount = maxElementCount; // TODO: Fix this
+ Clay__defaultMaxMeasureTextWordCacheCount = maxElementCount * 2;
+ }
+}
+
+CLAY_WASM_EXPORT("Clay_GetMaxMeasureTextCacheWordCount")
+int32_t Clay_GetMaxMeasureTextCacheWordCount(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ return context->maxMeasureTextCacheWordCount;
+}
+
+CLAY_WASM_EXPORT("Clay_SetMaxMeasureTextCacheWordCount")
+void Clay_SetMaxMeasureTextCacheWordCount(int32_t maxMeasureTextCacheWordCount) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ if (context) {
+ Clay__currentContext->maxMeasureTextCacheWordCount = maxMeasureTextCacheWordCount;
+ } else {
+ Clay__defaultMaxMeasureTextWordCacheCount = maxMeasureTextCacheWordCount; // TODO: Fix this
+ }
+}
+
+CLAY_WASM_EXPORT("Clay_ResetMeasureTextCache")
+void Clay_ResetMeasureTextCache(void) {
+ Clay_Context* context = Clay_GetCurrentContext();
+ context->measureTextHashMapInternal.length = 0;
+ context->measureTextHashMapInternalFreeList.length = 0;
+ context->measureTextHashMap.length = 0;
+ context->measuredWords.length = 0;
+ context->measuredWordsFreeList.length = 0;
+
+ for (int32_t i = 0; i < context->measureTextHashMap.capacity; ++i) {
+ context->measureTextHashMap.internalArray[i] = 0;
+ }
+ context->measureTextHashMapInternal.length = 1; // Reserve the 0 value to mean "no next element"
+}
+
+#define CLAY__LERP(from, to, mix) (from + (to - from) * mix)
+
+CLAY_DLL_EXPORT bool Clay_EaseOut(Clay_TransitionCallbackArguments arguments) {
+ float ratio = 1;
+ if (arguments.duration > 0) {
+ ratio = CLAY__MIN(arguments.elapsedTime / arguments.duration, 1);
+ }
+ float inverse = 1.0f - ratio;
+ float lerpAmount = 1.0f - (inverse * inverse * inverse);
+ if (arguments.properties & CLAY_TRANSITION_PROPERTY_X) {
+ arguments.current->boundingBox.x = CLAY__LERP(arguments.initial.boundingBox.x, arguments.target.boundingBox.x, lerpAmount);
+ }
+ if (arguments.properties & CLAY_TRANSITION_PROPERTY_Y) {
+ arguments.current->boundingBox.y = CLAY__LERP(arguments.initial.boundingBox.y, arguments.target.boundingBox.y, lerpAmount);
+ }
+ if (arguments.properties & CLAY_TRANSITION_PROPERTY_WIDTH) {
+ arguments.current->boundingBox.width = CLAY__LERP(arguments.initial.boundingBox.width, arguments.target.boundingBox.width, lerpAmount);
+ }
+ if (arguments.properties & CLAY_TRANSITION_PROPERTY_HEIGHT) {
+ arguments.current->boundingBox.height = CLAY__LERP(arguments.initial.boundingBox.height, arguments.target.boundingBox.height, lerpAmount);
+ }
+ if (arguments.properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) {
+ arguments.current->backgroundColor = CLAY__INIT(Clay_Color) {
+ .r = CLAY__LERP(arguments.initial.backgroundColor.r, arguments.target.backgroundColor.r, lerpAmount),
+ .g = CLAY__LERP(arguments.initial.backgroundColor.g, arguments.target.backgroundColor.g, lerpAmount),
+ .b = CLAY__LERP(arguments.initial.backgroundColor.b, arguments.target.backgroundColor.b, lerpAmount),
+ .a = CLAY__LERP(arguments.initial.backgroundColor.a, arguments.target.backgroundColor.a, lerpAmount),
+ };
+ }
+ if (arguments.properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) {
+ arguments.current->overlayColor = CLAY__INIT(Clay_Color) {
+ .r = CLAY__LERP(arguments.initial.overlayColor.r, arguments.target.overlayColor.r, lerpAmount),
+ .g = CLAY__LERP(arguments.initial.overlayColor.g, arguments.target.overlayColor.g, lerpAmount),
+ .b = CLAY__LERP(arguments.initial.overlayColor.b, arguments.target.overlayColor.b, lerpAmount),
+ .a = CLAY__LERP(arguments.initial.overlayColor.a, arguments.target.overlayColor.a, lerpAmount),
+ };
+ }
+ if (arguments.properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) {
+ arguments.current->borderColor = CLAY__INIT(Clay_Color) {
+ .r = CLAY__LERP(arguments.initial.borderColor.r, arguments.target.borderColor.r, lerpAmount),
+ .g = CLAY__LERP(arguments.initial.borderColor.g, arguments.target.borderColor.g, lerpAmount),
+ .b = CLAY__LERP(arguments.initial.borderColor.b, arguments.target.borderColor.b, lerpAmount),
+ .a = CLAY__LERP(arguments.initial.borderColor.a, arguments.target.borderColor.a, lerpAmount),
+ };
+ }
+ if (arguments.properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) {
+ arguments.current->borderWidth = CLAY__INIT(Clay_BorderWidth) {
+ .left = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.left, arguments.target.borderWidth.left, lerpAmount),
+ .right = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.right, arguments.target.borderWidth.right, lerpAmount),
+ .top = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.top, arguments.target.borderWidth.top, lerpAmount),
+ .bottom = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.bottom, arguments.target.borderWidth.bottom, lerpAmount),
+ .betweenChildren = (uint16_t)CLAY__LERP(arguments.initial.borderWidth.betweenChildren, arguments.target.borderWidth.betweenChildren, lerpAmount),
+ };
+ }
+ return ratio >= 1;
+}
+
+#endif // CLAY_IMPLEMENTATION
+
+/*
+LICENSE
+zlib/libpng license
+
+Copyright (c) 2024 Nic Barker
+
+This software is provided 'as-is', without any express or implied warranty.
+In no event will the authors be held liable for any damages arising from the
+use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software in a
+ product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must not
+ be misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any source
+ distribution.
+*/
diff --git a/vendor/clay_renderer_raylib.c b/vendor/clay_renderer_raylib.c
new file mode 100644
index 0000000..7fbc2c7
--- /dev/null
+++ b/vendor/clay_renderer_raylib.c
@@ -0,0 +1,321 @@
+#include "raylib.h"
+#include "raymath.h"
+#include "stdint.h"
+#include "string.h"
+#include "stdio.h"
+#include "stdlib.h"
+
+#define CLAY_RECTANGLE_TO_RAYLIB_RECTANGLE(rectangle) (Rectangle) { .x = rectangle.x, .y = rectangle.y, .width = rectangle.width, .height = rectangle.height }
+#define CLAY_COLOR_TO_RAYLIB_COLOR(color) (Color) { .r = (unsigned char)roundf(color.r), .g = (unsigned char)roundf(color.g), .b = (unsigned char)roundf(color.b), .a = (unsigned char)roundf(color.a) }
+
+Camera Raylib_camera;
+
+typedef enum
+{
+ CUSTOM_LAYOUT_ELEMENT_TYPE_3D_MODEL
+} CustomLayoutElementType;
+
+typedef struct
+{
+ Model model;
+ float scale;
+ Vector3 position;
+ Matrix rotation;
+} CustomLayoutElement_3DModel;
+
+typedef struct
+{
+ CustomLayoutElementType type;
+ union {
+ CustomLayoutElement_3DModel model;
+ } customData;
+} CustomLayoutElement;
+
+const char* overlayShaderCode = "#version 330\n"
+ "\n"
+ "in vec2 fragTexCoord;\n"
+ "in vec4 fragColor;\n"
+ "\n"
+ "uniform sampler2D texture0;\n"
+ "uniform vec4 overlayColor;\n"
+ "\n"
+ "out vec4 finalColor;\n"
+ "\n"
+ "void main()\n"
+ "{\n"
+ " vec4 texelColor = texture(texture0, fragTexCoord) * fragColor;\n"
+ "\n"
+ " vec3 blendedRGB = mix(texelColor.rgb, overlayColor.rgb, overlayColor.a);\n"
+ "\n"
+ " finalColor = vec4(blendedRGB, texelColor.a);\n"
+ "}";
+
+Shader overlayShader;
+int colorLoc;
+bool overlayEnabled = false;
+
+void InitOverlay() {
+ overlayShader = LoadShaderFromMemory(0, overlayShaderCode);
+ colorLoc = GetShaderLocation(overlayShader, "overlayColor");
+}
+
+void SetColorOverlay(Color color) {
+ overlayEnabled = true;
+ float colorFloat[4] = {
+ (float)color.r/255.0f,
+ (float)color.g/255.0f,
+ (float)color.b/255.0f,
+ (float)color.a/255.0f,
+ };
+
+ SetShaderValue(overlayShader, colorLoc, colorFloat, SHADER_UNIFORM_VEC4);
+ BeginShaderMode(overlayShader);
+}
+
+void DisableColorOverlay() {
+ if (overlayEnabled) {
+ EndShaderMode();
+ overlayEnabled = false;
+ }
+}
+
+// Get a ray trace from the screen position (i.e mouse) within a specific section of the screen
+Ray GetScreenToWorldPointWithZDistance(Vector2 position, Camera camera, int screenWidth, int screenHeight, float zDistance)
+{
+ Ray ray = { 0 };
+
+ // Calculate normalized device coordinates
+ // NOTE: y value is negative
+ float x = (2.0f*position.x)/(float)screenWidth - 1.0f;
+ float y = 1.0f - (2.0f*position.y)/(float)screenHeight;
+ float z = 1.0f;
+
+ // Store values in a vector
+ Vector3 deviceCoords = { x, y, z };
+
+ // Calculate view matrix from camera look at
+ Matrix matView = MatrixLookAt(camera.position, camera.target, camera.up);
+
+ Matrix matProj = MatrixIdentity();
+
+ if (camera.projection == CAMERA_PERSPECTIVE)
+ {
+ // Calculate projection matrix from perspective
+ matProj = MatrixPerspective(camera.fovy*DEG2RAD, ((double)screenWidth/(double)screenHeight), 0.01f, zDistance);
+ }
+ else if (camera.projection == CAMERA_ORTHOGRAPHIC)
+ {
+ double aspect = (double)screenWidth/(double)screenHeight;
+ double top = camera.fovy/2.0;
+ double right = top*aspect;
+
+ // Calculate projection matrix from orthographic
+ matProj = MatrixOrtho(-right, right, -top, top, 0.01, 1000.0);
+ }
+
+ // Unproject far/near points
+ Vector3 nearPoint = Vector3Unproject((Vector3){ deviceCoords.x, deviceCoords.y, 0.0f }, matProj, matView);
+ Vector3 farPoint = Vector3Unproject((Vector3){ deviceCoords.x, deviceCoords.y, 1.0f }, matProj, matView);
+
+ // Calculate normalized direction vector
+ Vector3 direction = Vector3Normalize(Vector3Subtract(farPoint, nearPoint));
+
+ ray.position = farPoint;
+
+ // Apply calculated vectors to ray
+ ray.direction = direction;
+
+ return ray;
+}
+
+
+static inline Clay_Dimensions Raylib_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
+ // Measure string size for Font
+ Clay_Dimensions textSize = { 0 };
+
+ float maxTextWidth = 0.0f;
+ float lineTextWidth = 0;
+ int maxLineCharCount = 0;
+ int lineCharCount = 0;
+
+ float textHeight = config->fontSize;
+ Font* fonts = (Font*)userData;
+ Font fontToUse = fonts[config->fontId];
+ // Font failed to load, likely the fonts are in the wrong place relative to the execution dir.
+ // RayLib ships with a default font, so we can continue with that built in one.
+ if (!fontToUse.glyphs) {
+ fontToUse = GetFontDefault();
+ }
+
+ float scaleFactor = config->fontSize/(float)fontToUse.baseSize;
+
+ for (int i = 0; i < text.length; ++i, lineCharCount++)
+ {
+ if (text.chars[i] == '\n') {
+ maxTextWidth = fmax(maxTextWidth, lineTextWidth);
+ maxLineCharCount = CLAY__MAX(maxLineCharCount, lineCharCount);
+ lineTextWidth = 0;
+ lineCharCount = 0;
+ continue;
+ }
+ int index = text.chars[i] - 32;
+ if (fontToUse.glyphs[index].advanceX != 0) lineTextWidth += fontToUse.glyphs[index].advanceX;
+ else lineTextWidth += (fontToUse.recs[index].width + fontToUse.glyphs[index].offsetX);
+ }
+
+ maxTextWidth = fmax(maxTextWidth, lineTextWidth);
+ maxLineCharCount = CLAY__MAX(maxLineCharCount, lineCharCount);
+
+ textSize.width = maxTextWidth * scaleFactor + (lineCharCount * config->letterSpacing);
+ textSize.height = textHeight;
+
+ return textSize;
+}
+
+void Clay_Raylib_Initialize(int width, int height, const char *title, unsigned int flags) {
+ SetConfigFlags(flags);
+ InitWindow(width, height, title);
+ InitOverlay();
+// EnableEventWaiting();
+}
+
+// A MALLOC'd buffer, that we keep modifying inorder to save from so many Malloc and Free Calls.
+// Call Clay_Raylib_Close() to free
+static char *temp_render_buffer = NULL;
+static int temp_render_buffer_len = 0;
+
+// Call after closing the window to clean up the render buffer
+void Clay_Raylib_Close()
+{
+ if(temp_render_buffer) free(temp_render_buffer);
+ temp_render_buffer_len = 0;
+
+ CloseWindow();
+}
+
+
+void Clay_Raylib_Render(Clay_RenderCommandArray renderCommands, Font* fonts)
+{
+ for (int j = 0; j < renderCommands.length; j++)
+ {
+ Clay_RenderCommand *renderCommand = Clay_RenderCommandArray_Get(&renderCommands, j);
+ Clay_BoundingBox boundingBox = {renderCommand->boundingBox.x, renderCommand->boundingBox.y, renderCommand->boundingBox.width, renderCommand->boundingBox.height};
+ switch (renderCommand->commandType)
+ {
+ case CLAY_RENDER_COMMAND_TYPE_TEXT: {
+ Clay_TextRenderData *textData = &renderCommand->renderData.text;
+ Font fontToUse = fonts[textData->fontId];
+
+ int strlen = textData->stringContents.length + 1;
+
+ if(strlen > temp_render_buffer_len) {
+ // Grow the temp buffer if we need a larger string
+ if(temp_render_buffer) free(temp_render_buffer);
+ temp_render_buffer = (char *) malloc(strlen);
+ temp_render_buffer_len = strlen;
+ }
+
+ // Raylib uses standard C strings so isn't compatible with cheap slices, we need to clone the string to append null terminator
+ memcpy(temp_render_buffer, textData->stringContents.chars, textData->stringContents.length);
+ temp_render_buffer[textData->stringContents.length] = '\0';
+ DrawTextEx(fontToUse, temp_render_buffer, (Vector2){boundingBox.x, boundingBox.y}, (float)textData->fontSize, (float)textData->letterSpacing, CLAY_COLOR_TO_RAYLIB_COLOR(textData->textColor));
+
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_IMAGE: {
+ Texture2D imageTexture = *(Texture2D *)renderCommand->renderData.image.imageData;
+ Clay_Color tintColor = renderCommand->renderData.image.backgroundColor;
+ if (tintColor.r == 0 && tintColor.g == 0 && tintColor.b == 0 && tintColor.a == 0) {
+ tintColor = (Clay_Color) { 255, 255, 255, 255 };
+ }
+ DrawTexturePro(
+ imageTexture,
+ (Rectangle) { 0, 0, imageTexture.width, imageTexture.height },
+ (Rectangle){boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height},
+ (Vector2) {},
+ 0,
+ CLAY_COLOR_TO_RAYLIB_COLOR(tintColor));
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
+ BeginScissorMode((int)roundf(boundingBox.x), (int)roundf(boundingBox.y), (int)roundf(boundingBox.width), (int)roundf(boundingBox.height));
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
+ EndScissorMode();
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START: {
+ SetColorOverlay(CLAY_COLOR_TO_RAYLIB_COLOR(renderCommand->renderData.overlayColor.color));
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END: {
+ DisableColorOverlay();
+ }
+ case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
+ Clay_RectangleRenderData *config = &renderCommand->renderData.rectangle;
+ if (config->cornerRadius.topLeft > 0) {
+ float radius = (config->cornerRadius.topLeft * 2) / (float)((boundingBox.width > boundingBox.height) ? boundingBox.height : boundingBox.width);
+ DrawRectangleRounded((Rectangle) { boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height }, radius, 8, CLAY_COLOR_TO_RAYLIB_COLOR(config->backgroundColor));
+ } else {
+ DrawRectangle(boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height, CLAY_COLOR_TO_RAYLIB_COLOR(config->backgroundColor));
+ }
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_BORDER: {
+ Clay_BorderRenderData *config = &renderCommand->renderData.border;
+ // Left border
+ if (config->width.left > 0) {
+ DrawRectangleV((Vector2) { boundingBox.x, boundingBox.y + config->cornerRadius.topLeft }, (Vector2) { config->width.left, boundingBox.height - config->cornerRadius.topLeft - config->cornerRadius.bottomLeft }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+ }
+ // Right border
+ if (config->width.right > 0) {
+ DrawRectangleV((Vector2) { boundingBox.x + boundingBox.width - config->width.right, boundingBox.y + config->cornerRadius.topRight }, (Vector2) { config->width.right, boundingBox.height - config->cornerRadius.topRight - config->cornerRadius.bottomRight }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+ }
+ // Top border
+ if (config->width.top > 0) {
+ DrawRectangleV((Vector2) { boundingBox.x + config->cornerRadius.topLeft, boundingBox.y }, (Vector2) { boundingBox.width - config->cornerRadius.topLeft - config->cornerRadius.topRight, (int)config->width.top }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+ }
+ // Bottom border
+ if (config->width.bottom > 0) {
+ DrawRectangleV((Vector2) { boundingBox.x + config->cornerRadius.bottomLeft, boundingBox.y + boundingBox.height - config->width.bottom }, (Vector2) { boundingBox.width - config->cornerRadius.bottomLeft - config->cornerRadius.bottomRight, (int)config->width.bottom }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+ }
+ if (config->cornerRadius.topLeft > 0) {
+ DrawRing((Vector2) { roundf(boundingBox.x + config->cornerRadius.topLeft), roundf(boundingBox.y + config->cornerRadius.topLeft) }, roundf(config->cornerRadius.topLeft - config->width.top), config->cornerRadius.topLeft, 180, 270, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+ }
+ if (config->cornerRadius.topRight > 0) {
+ DrawRing((Vector2) { roundf(boundingBox.x + boundingBox.width - config->cornerRadius.topRight), roundf(boundingBox.y + config->cornerRadius.topRight) }, roundf(config->cornerRadius.topRight - config->width.top), config->cornerRadius.topRight, 270, 360, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+ }
+ if (config->cornerRadius.bottomLeft > 0) {
+ DrawRing((Vector2) { roundf(boundingBox.x + config->cornerRadius.bottomLeft), roundf(boundingBox.y + boundingBox.height - config->cornerRadius.bottomLeft) }, roundf(config->cornerRadius.bottomLeft - config->width.bottom), config->cornerRadius.bottomLeft, 90, 180, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+ }
+ if (config->cornerRadius.bottomRight > 0) {
+ DrawRing((Vector2) { roundf(boundingBox.x + boundingBox.width - config->cornerRadius.bottomRight), roundf(boundingBox.y + boundingBox.height - config->cornerRadius.bottomRight) }, roundf(config->cornerRadius.bottomRight - config->width.bottom), config->cornerRadius.bottomRight, 0.1, 90, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
+ }
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_CUSTOM: {
+ Clay_CustomRenderData *config = &renderCommand->renderData.custom;
+ CustomLayoutElement *customElement = (CustomLayoutElement *)config->customData;
+ if (!customElement) continue;
+ switch (customElement->type) {
+ case CUSTOM_LAYOUT_ELEMENT_TYPE_3D_MODEL: {
+ Clay_BoundingBox rootBox = renderCommands.internalArray[0].boundingBox;
+ float scaleValue = CLAY__MIN(CLAY__MIN(1, 768 / rootBox.height) * CLAY__MAX(1, rootBox.width / 1024), 1.5f);
+ Ray positionRay = GetScreenToWorldPointWithZDistance((Vector2) { renderCommand->boundingBox.x + renderCommand->boundingBox.width / 2, renderCommand->boundingBox.y + (renderCommand->boundingBox.height / 2) + 20 }, Raylib_camera, (int)roundf(rootBox.width), (int)roundf(rootBox.height), 140);
+ BeginMode3D(Raylib_camera);
+ DrawModel(customElement->customData.model.model, positionRay.position, customElement->customData.model.scale * scaleValue, WHITE); // Draw 3d model with texture
+ EndMode3D();
+ break;
+ }
+ default: break;
+ }
+ break;
+ }
+ default: {
+ printf("Error: unhandled render command.");
+ exit(1);
+ }
+ }
+ }
+}