[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay bomb.sh/docs/tty
Something went wrong. Try again.
C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317/* clayterm.c — WASM terminal rendering engine for Clay UI * * Public API (exported to WASM): * clayterm_size — compute arena size for given dimensions * init — initialize a Clayterm instance in provided memory * reduce — decode command buffer, run Clay layout, render to ANSI * output — pointer to output byte buffer * length — length of output byte buffer * measure — Clay text measurement callback * error_count, error_type, error_message_length, error_message_ptr * — per-render Clay error accessors */
#include "clayterm.h"#include "transitions.h"#include "../clay/clay.h"#include "buffer.h"#include "cell.h"#include "mem.h"#include "utf8.h"#include "wcwidth.h"
/* Module-level pointer to the Term currently executing reduce(). * Set/cleared around each render pass so transition handlers (which Clay * invokes with no userData — see Clay_TransitionCallbackArguments) can * report back to the right Term's animating_count. Revisit once * nicbarker/clay#603 lands userData on transition callbacks; then the * handler can resolve its Term from args directly and this can go away. */struct Clayterm *ct_active_context = NULL;
/* ── Command buffer protocol ──────────────────────────────────────── */
#define OP_BEGIN_LAYOUT 0x01#define OP_OPEN_ELEMENT 0x02#define OP_TEXT 0x03#define OP_CLOSE_ELEMENT 0x04#define OP_END_LAYOUT 0x05
#define PROP_LAYOUT 0x01#define PROP_BG_COLOR 0x02#define PROP_CORNER_RADIUS 0x04#define PROP_BORDER 0x08#define PROP_CLIP 0x10#define PROP_FLOATING 0x20#define PROP_TRANSITION 0x40
/* ── Instance state ───────────────────────────────────────────────── */
#define MAX_ERRORS 32
/* clip stack depth: nesting beyond this clamps to the deepest rect */#define CLIP_STACK_MAX 16
/* Clayterm-specific error codes, numbered past Clay's error enum (0..8). * Mirrored by ERROR_TYPES in term.ts. */#define CLAYTERM_ERR_CLIP_DEPTH_EXCEEDED 9#define CLAYTERM_ERR_COMBINING_MARKS_EXCEEDED 10
#define CLAYTERM_STR_(x) #x#define CLAYTERM_STR(x) CLAYTERM_STR_(x)
typedef struct { int x, y, w, h;} ClipRect;
struct Clayterm { int w, h; Cell *front; Cell *back; Buffer out; uint32_t lastfg, lastbg; int lastx, lasty; /* Color capability evidence pushed by the host (color-encoding-spec * §4.2). Authoritative for emission; the host re-pushes after any * re-init, so the values survive resize updates. */ uint32_t colors; /* max_colors evidence; §7.1 baseline is 256 */ int truecolor; /* positive truecolor evidence */ /* Terminal-default colors for compositing destinations (renderer-spec * §7.9), resolved by the host: reported theme > createTerm * defaultTheme > black background / white foreground. 24-bit RGB. */ uint32_t default_bg; uint32_t default_fg; /* Emission tier, resolved once per reduce. last_tier detects tier * changes, which force a complete redraw (color-encoding-spec CI-5). */ int tier; int last_tier; /* clip region (active top mirrored here so setcell stays unchanged) */ int clipx, clipy, clipw, cliph; int clipping; /* clip stack: nesting pushes intersected rects, leaving pops to restore */ ClipRect clipstack[CLIP_STACK_MAX]; int clipdepth; /* untracked clip levels open beyond CLIP_STACK_MAX; popped without * touching the tracked stack so push/pop stays symmetric */ int clipoverflow; /* set once per frame when nesting first exceeds the tracked depth */ int clip_depth_exceeded; /* set once per frame when a cell first runs out of combining-mark slots */ int combining_exceeded; /* error collection */ Clay_ErrorData errors[MAX_ERRORS]; int error_count; int animating_count; /* Caret state for hardware-cursor management. The renderer records the * first text node carrying a caret declaration per frame, precomputes the * caret's byte offset into that node's content, then places the cell as * a side effect of render_text's walk. had_caret_last_frame is the only * cross-frame bit retained. */ const char *caret_text_chars; /* start of caret-bearing text node's bytes, or NULL */ int caret_text_length; /* byte length of that text node */ uint32_t caret_offset_bytes; /* target byte offset within that text node */ int caret_placed; /* 1 once the render walk has recorded a cell */ int caret_tail_valid; /* 1 when caret_tail_x/y hold a fallback cell */ int caret_tail_x, caret_tail_y; /* trailing cell of the most recently walked caret slice */ int caret_x, caret_y; /* resolved cell (column, row); -1 sentinel until placed */ int has_caret; /* 1 when the current frame will emit a cursor */ int had_caret_last_frame; /* 1 when the previous frame emitted a cursor */};
/* Memory layout inside the arena provided by the host: * [Clayterm struct] [front cells] [back cells] [output buffer] * * Output buffer is sized at 64 bytes per cell — enough for worst-case * full-screen redraws with truecolor SGR sequences on every cell. *//* 128 bytes per cell: ~84 bytes worst-case for CUP + SGR sequences, plus up * to 4 (base SMP char) + 8×4 (combining marks) = 36 bytes of cluster text. */#define OUT_BYTES_PER_CELL 128
/* ── Cell buffer ops ──────────────────────────────────────────────── */
static Cell *cell_at(struct Clayterm *ct, Cell *buf, int x, int y) { return &buf[y * ct->w + x];}
static int setcell(struct Clayterm *ct, int x, int y, uint32_t ch, uint32_t fg, uint32_t bg) { if (x < 0 || x >= ct->w || y < 0 || y >= ct->h) return 0; if (ct->clipping) { if (x < ct->clipx || x >= ct->clipx + ct->clipw) return 0; if (y < ct->clipy || y >= ct->clipy + ct->cliph) return 0; } Cell *c = cell_at(ct, ct->back, x, y); c->ch = ch; c->fg = fg; if (!(bg & ATTR_DEFAULT)) { c->bg = bg; } for (int i = 0; i < CELL_MAX_COMBINING; i++) c->combining[i] = 0; return 1;}
/* Surface a COMBINING_MARKS_EXCEEDED error once per frame. */static void report_combining_exceeded(struct Clayterm *ct) { if (ct->combining_exceeded) return; ct->combining_exceeded = 1; if (ct->error_count >= MAX_ERRORS) return; static const char msg[] = "cell exceeds combining-mark limit of " CLAYTERM_STR( CELL_MAX_COMBINING) "; excess marks truncated"; ct->errors[ct->error_count++] = (Clay_ErrorData){ .errorType = (Clay_ErrorType)CLAYTERM_ERR_COMBINING_MARKS_EXCEEDED, .errorText = {.isStaticallyAllocated = true, .length = (int32_t)(sizeof(msg) - 1), .chars = msg}, .userData = ct, };}
/* Append a combining-mark codepoint to the cell at (x, y) in the back buffer. * Marks beyond CELL_MAX_COMBINING are dropped (truncation from end) and * reported once per frame. */static void append_combining(struct Clayterm *ct, int x, int y, uint32_t cp) { if (x < 0 || x >= ct->w || y < 0 || y >= ct->h) return; Cell *c = cell_at(ct, ct->back, x, y); for (int i = 0; i < CELL_MAX_COMBINING; i++) { if (c->combining[i] == 0) { c->combining[i] = cp; return; } } report_combining_exceeded(ct);}
/* ── Color tiers and downmapping ──────────────────────────────────── */
/* Emission tiers (color-encoding-spec §6.1). */#define TIER_TRUECOLOR 0#define TIER_256 1#define TIER_16 2
/* Nominal 16-color palette (color-encoding-spec §6.3.1): the renderer's * canonical model of what each 4-bit index means for downmapping. Pure * primaries for 0-7, pastel brights for 8-15. Not a claim about any * terminal's actual palette. */static const uint32_t NOMINAL_PALETTE[16] = { 0x000000, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xc0c0c0, 0x808080, 0xff5555, 0x55ff55, 0xffff55, 0x5555ff, 0xff55ff, 0x55ffff, 0xffffff,};
/* 6x6x6 color cube channel values (indices 16-231). */static const int CUBE_STEPS[6] = {0, 95, 135, 175, 215, 255};
/* Resolve the emission tier from the capability evidence pushed by the * host (color-encoding-spec §6.1). The colors <= 16 boundary matches the * colordepth denial mapping in terminfo-spec §6.3. */static int resolve_tier(const struct Clayterm *ct) { if (ct->truecolor) return TIER_TRUECOLOR; if (ct->colors <= 16) return TIER_16; return TIER_256;}
/* Nearest nominal palette entry by squared Euclidean distance; ties * resolve to the lowest index (color-encoding-spec §6.3.2). */static int map_16(uint32_t rgb) { int r = (rgb >> 16) & 0xff; int g = (rgb >> 8) & 0xff; int b = rgb & 0xff; int best = 0; int best_dist = -1; for (int i = 0; i < 16; i++) { uint32_t c = NOMINAL_PALETTE[i]; int dr = r - (int)((c >> 16) & 0xff); int dg = g - (int)((c >> 8) & 0xff); int db = b - (int)(c & 0xff); int dist = dr * dr + dg * dg + db * db; if (best_dist < 0 || dist < best_dist) { best_dist = dist; best = i; } } return best;}
/* The cube step for an exact cube value, or -1 when the value is not a * cube channel value. */static int cube_exact(int v) { for (int i = 0; i < 6; i++) { if (CUBE_STEPS[i] == v) return i; } return -1;}
/* Nearest cube step for an arbitrary channel value; ties resolve to the * smaller value (color-encoding-spec §6.3.3 rule 4). */static int cube_step(int v) { int best = 0; int best_dist = -1; for (int i = 0; i < 6; i++) { int d = v - CUBE_STEPS[i]; if (d < 0) d = -d; if (best_dist < 0 || d < best_dist) { best_dist = d; best = i; } } return best;}
/* Downmap a 24-bit RGB value to a 256-palette index. Ordered rules; * the first match wins (color-encoding-spec §6.3.3). Rules 1-3 make * every 256-palette entry resolve to an index of the identical color * (CI-4; the nominal index wins where the palettes overlap). */static int map_256(uint32_t rgb) { int r = (rgb >> 16) & 0xff; int g = (rgb >> 8) & 0xff; int b = rgb & 0xff;
/* 1. exact nominal match: standard colors keep their 4-bit indices */ for (int i = 0; i < 16; i++) { if (NOMINAL_PALETTE[i] == rgb) return i; }
/* 2. exact cube match */ int cr = cube_exact(r); int cg = cube_exact(g); int cb = cube_exact(b); if (cr >= 0 && cg >= 0 && cb >= 0) return 16 + 36 * cr + 6 * cg + cb;
/* 3. grayscale: nearest of the ramp extended by both endpoints. * Candidates run in ascending index order (16, 231, 232..255), so a * strict comparison resolves ties to the lowest index. */ if (r == g && g == b) { int best = 16; int best_dist = r; /* value 0 at index 16 */ int d = 255 - r; /* value 255 at index 231 */ if (d < best_dist) { best = 231; best_dist = d; } for (int i = 0; i < 24; i++) { int val = 8 + 10 * i; d = r - val; if (d < 0) d = -d; if (d < best_dist) { best = 232 + i; best_dist = d; } } return best; }
/* 4. nearest cube value per channel */ return 16 + 36 * cube_step(r) + 6 * cube_step(g) + cube_step(b);}
/* ── Escape sequence generation ───────────────────────────────────── */
/* Emit the SGR color sequence for one packed RGB value under the active * tier (color-encoding-spec §6.2). `is_fg` selects the foreground * (38, 3x/9x) or background (48, 4x/10x) code family. */static void emit_sgr_color(struct Clayterm *ct, uint32_t rgb, int is_fg) { switch (ct->tier) { case TIER_16: { int idx = map_16(rgb); int base = is_fg ? 30 : 40; if (idx >= 8) base += 60; /* aixterm bright codes: 90-97 / 100-107 */ buf_str(&ct->out, "\x1b["); buf_num(&ct->out, base + (idx & 7)); buf_put(&ct->out, "m", 1); break; } case TIER_256: buf_str(&ct->out, is_fg ? "\x1b[38;5;" : "\x1b[48;5;"); buf_num(&ct->out, map_256(rgb)); buf_put(&ct->out, "m", 1); break; default: /* TIER_TRUECOLOR */ buf_str(&ct->out, is_fg ? "\x1b[38;2;" : "\x1b[48;2;"); buf_num(&ct->out, (rgb >> 16) & 0xff); buf_put(&ct->out, ";", 1); buf_num(&ct->out, (rgb >> 8) & 0xff); buf_put(&ct->out, ";", 1); buf_num(&ct->out, rgb & 0xff); buf_put(&ct->out, "m", 1); }}
static void emit_attr(struct Clayterm *ct, uint32_t fg, uint32_t bg) { if (fg == ct->lastfg && bg == ct->lastbg) return;
/* SGR reset */ buf_str(&ct->out, "\x1b[0m");
/* style attributes from fg high byte */ if (fg & ATTR_BOLD) buf_str(&ct->out, "\x1b[1m"); if (fg & ATTR_DIM) buf_str(&ct->out, "\x1b[2m"); if (fg & ATTR_ITALIC) buf_str(&ct->out, "\x1b[3m"); if (fg & ATTR_UNDERLINE) buf_str(&ct->out, "\x1b[4m"); if (fg & ATTR_BLINK) buf_str(&ct->out, "\x1b[5m"); if (fg & ATTR_REVERSE) buf_str(&ct->out, "\x1b[7m"); if (fg & ATTR_STRIKEOUT) buf_str(&ct->out, "\x1b[9m");
/* foreground */ if (!(fg & ATTR_DEFAULT)) emit_sgr_color(ct, fg & COLOR_MASK, 1);
/* background */ if (!(bg & ATTR_DEFAULT)) emit_sgr_color(ct, bg & COLOR_MASK, 0);
ct->lastfg = fg; ct->lastbg = bg;}
/** Emit CUP sequence. `row` is the 1-based terminal row of the region. */static void emit_cursor(struct Clayterm *ct, int x, int y, int row) { buf_str(&ct->out, "\x1b["); buf_num(&ct->out, y + row); buf_put(&ct->out, ";", 1); buf_num(&ct->out, x + 1); buf_put(&ct->out, "H", 1);}
static void emit_ch(struct Clayterm *ct, int x, int y, int row, uint32_t ch) { if (ct->lastx != x - 1 || ct->lasty != y) { emit_cursor(ct, x, y, row); } ct->lastx = x; ct->lasty = y;
if (!iswprint(ch)) ch = 0xfffd; buf_char(&ct->out, ch);}
/** * Diff back buffer against front buffer and emit only changed cells * using absolute CUP positioning (\x1b[row;colH). Unchanged cells are * skipped, making this efficient for subsequent frames where most of * the screen is static. Derived from termbox2 tb_present. */static void present_cups(struct Clayterm *ct, int row) { ct->lastx = -1; ct->lasty = -1;
for (int y = 0; y < ct->h; y++) { for (int x = 0; x < ct->w;) { Cell *back = cell_at(ct, ct->back, x, y); Cell *front = cell_at(ct, ct->front, x, y);
int w = wcwidth(back->ch); if (w < 1) w = 1;
if (cell_cmp(back, front)) { /* copy to front */ *front = *back;
emit_attr(ct, back->fg, back->bg);
if (w > 1 && x >= ct->w - (w - 1)) { /* wide char doesn't fit, send spaces */ for (int i = x; i < ct->w; i++) emit_ch(ct, i, y, row, ' '); } else { emit_ch(ct, x, y, row, back->ch); for (int ci = 0; ci < CELL_MAX_COMBINING && back->combining[ci]; ci++) buf_char(&ct->out, back->combining[ci]); /* mark trailing cells of wide char as invalid in front * so they'll diff when overwritten by narrow chars */ for (int i = 1; i < w; i++) { Cell *fw = cell_at(ct, ct->front, x + i, y); fw->ch = 0xffffffff; fw->fg = 0xffffffff; fw->bg = 0xffffffff; } } } x += w; } }
/* Hardware cursor management: per the spec's outcome contract, * leave the cursor visible at the caret cell if a caret was declared * this frame; otherwise hide it if and only if a caret was declared * last frame. When neither this frame nor any prior frame declared * a caret, emit nothing. */ if (ct->has_caret) { emit_cursor(ct, ct->caret_x, ct->caret_y, row); buf_str(&ct->out, "\x1b[?25h"); } else if (ct->had_caret_last_frame) { buf_str(&ct->out, "\x1b[?25l"); } ct->had_caret_last_frame = ct->has_caret;}
/** * Emit back buffer as newline-separated rows without CUP positioning. * Every cell is written (no diffing), and the front buffer is primed * so that a subsequent present() call can diff efficiently. This is * used for inline "region" rendering where the caller manages cursor * positioning externally and the output must work in pipes. */static void present_lines(struct Clayterm *ct) { for (int y = 0; y < ct->h; y++) { if (y > 0) buf_put(&ct->out, "\n", 1); for (int x = 0; x < ct->w;) { Cell *back = cell_at(ct, ct->back, x, y); Cell *front = cell_at(ct, ct->front, x, y);
int w = wcwidth(back->ch); if (w < 1) w = 1;
*front = *back;
emit_attr(ct, back->fg, back->bg);
if (w > 1 && x >= ct->w - (w - 1)) { for (int i = x; i < ct->w; i++) { buf_char(&ct->out, ' '); } } else { uint32_t ch = back->ch; if (!iswprint(ch)) ch = 0xfffd; buf_char(&ct->out, ch); for (int ci = 0; ci < CELL_MAX_COMBINING && back->combining[ci]; ci++) buf_char(&ct->out, back->combining[ci]); for (int i = 1; i < w; i++) { Cell *fw = cell_at(ct, ct->front, x + i, y); fw->ch = 0xffffffff; fw->fg = 0xffffffff; fw->bg = 0xffffffff; } } x += w; } }}
/* ── Color conversion ─────────────────────────────────────────────── */
static uint32_t color(Clay_Color c) { uint8_t r = (uint8_t)c.r; uint8_t g = (uint8_t)c.g; uint8_t b = (uint8_t)c.b; return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;}
/* ── Clay render backend ──────────────────────────────────────────── */
static void render_rect(struct Clayterm *ct, int x0, int y0, int x1, int y1, Clay_RectangleRenderData *r) { uint32_t bg = color(r->backgroundColor); for (int y = y0; y < y1; y++) for (int x = x0; x < x1; x++) setcell(ct, x, y, ' ', ATTR_DEFAULT, bg);}
/* Return the byte length of the first `cps` code points of `start`, * clamped to at most `max_bytes`. Used at decode time to convert the * caller's code-point caret offset into a stable byte offset. */static uint32_t utf8_bytes_for_cps(const char *start, uint32_t cps, uint32_t max_bytes) { uint32_t consumed = 0; const char *p = start; int rem = (int)max_bytes; for (uint32_t k = 0; k < cps && rem > 0; k++) { uint32_t cp; int n = utf8_decode(&cp, p); if (n <= 0) { n = 1; } if (n > rem) { n = rem; } consumed += (uint32_t)n; p += n; rem -= n; } return consumed;}
static void render_text(struct Clayterm *ct, int x0, int y0, Clay_RenderCommand *cmd) {
Clay_TextRenderData *t = &cmd->renderData.text; /* userData points at the text op's [bg, attrs] words in the command * buffer, which remains valid for the whole render pass (see the * OP_TEXT decode in reduce()). */ const uint32_t *s = (const uint32_t *)cmd->userData; if (s == NULL) { __builtin_trap(); } /* Alpha 0 encodes the terminal default bg (§7.9); anything else is * an explicit bg. The alpha byte is dropped here until compositing * (§7.9) consumes it. */ uint32_t bg_word = s[0]; uint32_t bg = (bg_word >> 24) ? (bg_word & 0x00FFFFFF) : ATTR_DEFAULT; uint32_t fg = color(t->textColor);
/* attrs ride the word after bg: bits 0-6 are SGR attribute flags, * bit 7 marks the terminal-default foreground. */ uint32_t attrs_byte = s[1]; fg |= (attrs_byte & 0x7f) << 24; if (attrs_byte & 0x80) fg |= ATTR_DEFAULT;
const char *slice = t->stringContents.chars; int slice_len = t->stringContents.length;
/* Determine whether this slice belongs to the caret's text node and * whether the caret still needs to be placed. If so, resolve the cell * as a side effect of the walk instead of doing a separate pass. */ const char *node_start = ct->caret_text_chars; const char *node_end = node_start ? node_start + ct->caret_text_length : NULL; int caret_relevant = (node_start != NULL && !ct->caret_placed && slice >= node_start && slice < node_end);
/* Pre-check: if the slice's first byte is already past the target, * whitespace at the wrap seam was dropped by the layout engine. Snap * the caret to this slice's origin. */ if (caret_relevant) { uint32_t slice_start_bytes = (uint32_t)(slice - node_start); if (slice_start_bytes > ct->caret_offset_bytes) { ct->caret_x = x0; ct->caret_y = y0; ct->caret_placed = 1; caret_relevant = 0; } }
const char *p = slice; int rem = slice_len; int x = x0; int last_x = -1; /* column of the most-recently written base cell */
while (rem > 0) { /* Check at the top of each iteration: if the pointer we are about to * consume matches the target byte offset, the caret sits at the * current cell — right before this code point. */ if (caret_relevant && (uint32_t)(p - node_start) == ct->caret_offset_bytes) { ct->caret_x = x; ct->caret_y = y0; ct->caret_placed = 1; caret_relevant = 0; }
uint32_t cp; int n = utf8_decode(&cp, p); if (n <= 0) { n = 1; cp = 0xfffd; } int cw = wcwidth(cp); if (cw < 0) cw = 1; if (cw > 0) { last_x = setcell(ct, x, y0, cp, fg, bg) ? x : -1; x += cw; } else if (last_x >= 0) { /* combining mark: attach to the preceding base cell */ append_combining(ct, last_x, y0, cp); } p += n; rem -= n; }
/* Remember the trailing cell of this slice as the end-of-content * fallback. If no later slice claims the target and the caret still * hasn't been placed by the end of the render command loop, this cell * is where a caret at offset == content-length lands. */ if (caret_relevant) { ct->caret_tail_x = x; ct->caret_tail_y = y0; ct->caret_tail_valid = 1; }}
static void render_border(struct Clayterm *ct, int x0, int y0, int x1, int y1, Clay_RenderCommand *cmd) { Clay_BorderRenderData *b = &cmd->renderData.border; /* Must match border packing in ops.ts. * userData points at eight required words in the command buffer: per * side (top, right, bottom, left order) a fg word (0xAARRGGBB with * true alpha — the fg is always explicit) and a bg word (0xAARRGGBB; * alpha 0 encodes the terminal default). Fallback resolution (shared * color/bg vs side overrides) happens on the TypeScript side; this * renderer consumes explicit values only. The command buffer outlives * the render pass within reduce(). Missing userData is a wire-format * violation. */ const uint32_t *s = (const uint32_t *)cmd->userData; if (s == NULL) { __builtin_trap(); }
/* Slice behavior-preserving: the fg alpha byte is dropped here until * compositing (§7.9) consumes it; alpha 0 bg maps to the default. */ uint32_t top_fg = s[0] & 0x00FFFFFF; uint32_t top_bg = (s[1] >> 24) ? (s[1] & 0x00FFFFFF) : ATTR_DEFAULT; uint32_t right_fg = s[2] & 0x00FFFFFF; uint32_t right_bg = (s[3] >> 24) ? (s[3] & 0x00FFFFFF) : ATTR_DEFAULT; uint32_t bot_fg = s[4] & 0x00FFFFFF; uint32_t bot_bg = (s[5] >> 24) ? (s[5] & 0x00FFFFFF) : ATTR_DEFAULT; uint32_t left_fg = s[6] & 0x00FFFFFF; uint32_t left_bg = (s[7] >> 24) ? (s[7] & 0x00FFFFFF) : ATTR_DEFAULT; int top = b->width.top > 0; int bot = b->width.bottom > 0; int left = b->width.left > 0; int right = b->width.right > 0;
/* corners — rounded when corner radius > 0. Drawn only when both adjacent * sides are enabled; a terminal cell holds a single fg/bg, so top corners * take the top side attributes and bottom corners take the bottom side * attributes (deterministic approximation of CSS split corners). */ uint32_t tl = b->cornerRadius.topLeft > 0 ? 0x256d : 0x250c; uint32_t tr = b->cornerRadius.topRight > 0 ? 0x256e : 0x2510; uint32_t bl = b->cornerRadius.bottomLeft > 0 ? 0x2570 : 0x2514; uint32_t br = b->cornerRadius.bottomRight > 0 ? 0x256f : 0x2518;
if (top && left) setcell(ct, x0, y0, tl, top_fg, top_bg); if (top && right) setcell(ct, x1 - 1, y0, tr, top_fg, top_bg); if (bot && left) setcell(ct, x0, y1 - 1, bl, bot_fg, bot_bg); if (bot && right) setcell(ct, x1 - 1, y1 - 1, br, bot_fg, bot_bg);
/* horizontal edges */ if (top) for (int x = x0 + left; x < x1 - right; x++) setcell(ct, x, y0, 0x2500, top_fg, top_bg); if (bot) for (int x = x0 + left; x < x1 - right; x++) setcell(ct, x, y1 - 1, 0x2500, bot_fg, bot_bg);
/* vertical edges — excluding joined corner cells owned by top/bottom */ if (left) for (int y = y0 + top; y < y1 - bot; y++) setcell(ct, x0, y, 0x2502, left_fg, left_bg); if (right) for (int y = y0 + top; y < y1 - bot; y++) setcell(ct, x1 - 1, y, 0x2502, right_fg, right_bg);}
/* ── Command buffer helpers ───────────────────────────────────────── */
static uint32_t rd(uint32_t *buf, int len, int *i) { if (*i < len) return buf[(*i)++]; return 0;}
static float rdf(uint32_t *buf, int len, int *i) { uint32_t v = rd(buf, len, i); float f; memcpy(&f, &v, 4); return f;}
static Clay_Color unpack_color(uint32_t c) { return (Clay_Color){ .r = (float)((c >> 16) & 0xff), .g = (float)((c >> 8) & 0xff), .b = (float)(c & 0xff), .a = (float)((c >> 24) & 0xff), };}
static Clay_SizingAxis decode_axis(uint32_t *buf, int len, int *i) { uint32_t type = rd(buf, len, i); float a = rdf(buf, len, i); float b = rdf(buf, len, i); Clay_SizingAxis axis = {0}; switch (type) { case 0: /* FIT */ axis.type = CLAY__SIZING_TYPE_FIT; axis.size.minMax.min = a; axis.size.minMax.max = b; break; case 1: /* GROW */ axis.type = CLAY__SIZING_TYPE_GROW; axis.size.minMax.min = a; axis.size.minMax.max = b; break; case 2: /* PERCENT */ axis.type = CLAY__SIZING_TYPE_PERCENT; axis.size.percent = a; break; case 3: /* FIXED */ axis.type = CLAY__SIZING_TYPE_FIXED; axis.size.minMax.min = a; axis.size.minMax.max = a; break; } return axis;}
/* ── Public API ───────────────────────────────────────────────────── */
static int align64(int n) { return (n + 63) & ~63; }
/* Clay's element capacity drives nearly all of Clay_MinMemorySize, so scale it * with the grid instead of shipping Clay's fixed 8192-element default. 2x cells * bounds sane trees (leaves >= 1 cell, branching >= 2); the ceiling preserves * today's capacity for large grids, the floor keeps headroom on tiny ones. * Must run before Clay_MinMemorySize in both clayterm_size and init so the * size the host queries always matches the arena init consumes. */static void set_clay_capacity(int w, int h) { int elems = w * h * 2; if (elems < 2048) elems = 2048; if (elems > 8192) elems = 8192; Clay_SetMaxElementCount(elems); Clay_SetMaxMeasureTextCacheWordCount(elems * 2);}
int clayterm_size(int w, int h) { set_clay_capacity(w, h); int cell_count = w * h; int cell_bytes = cell_count * (int)sizeof(Cell); int out_bytes = cell_count * OUT_BYTES_PER_CELL; int clay_bytes = (int)Clay_MinMemorySize(); return align8((int)sizeof(struct Clayterm)) + align8(cell_bytes) /* front */ + align8(cell_bytes) /* back */ + align8(out_bytes) /* output buffer */ + align64(clay_bytes); /* Clay arena */}
static void clay_error(Clay_ErrorData err) { struct Clayterm *ct = (struct Clayterm *)err.userData; if (ct->error_count < MAX_ERRORS) { ct->errors[ct->error_count++] = err; }}
/* Surface a CLIP_DEPTH_EXCEEDED error once per frame. The message is a static * literal, so its pointer lives in WASM linear memory and is readable by the * host via error_message_ptr/length. */static void report_clip_depth_exceeded(struct Clayterm *ct) { if (ct->clip_depth_exceeded) return; ct->clip_depth_exceeded = 1; if (ct->error_count >= MAX_ERRORS) return; static const char msg[] = "clip nesting exceeds tracked depth limit of " CLAYTERM_STR( CLIP_STACK_MAX) "; over-deep clips coalesced into the deepest " "tracked region"; ct->errors[ct->error_count++] = (Clay_ErrorData){ .errorType = (Clay_ErrorType)CLAYTERM_ERR_CLIP_DEPTH_EXCEEDED, .errorText = {.isStaticallyAllocated = true, .length = (int32_t)(sizeof(msg) - 1), .chars = msg}, .userData = ct, };}
int error_count(struct Clayterm *ct) { return ct->error_count; }
int error_type(struct Clayterm *ct, int index) { if (index < 0 || index >= ct->error_count) return -1; return (int)ct->errors[index].errorType;}
int error_message_length(struct Clayterm *ct, int index) { if (index < 0 || index >= ct->error_count) return 0; return ct->errors[index].errorText.length;}
int error_message_ptr(struct Clayterm *ct, int index) { if (index < 0 || index >= ct->error_count) return 0; return (int)ct->errors[index].errorText.chars;}
/* Push host-held color capability evidence into the instance * (color-encoding-spec §4.2; renderer-spec §7.9 defaults). Called once * after init and after update() folds capability events that change any * value; re-pushed by the host after any re-init so the values survive * resize updates. */void set_capabilities(struct Clayterm *ct, uint32_t colors, int truecolor, uint32_t default_bg, uint32_t default_fg) { ct->colors = colors; ct->truecolor = truecolor ? 1 : 0; ct->default_bg = default_bg & 0x00FFFFFF; ct->default_fg = default_fg & 0x00FFFFFF;}
struct Clayterm *init(void *mem, int w, int h) { set_clay_capacity(w, h); struct Clayterm *ct = (struct Clayterm *)mem; int cell_count = w * h; int cell_bytes = align8(cell_count * (int)sizeof(Cell)); int out_bytes = align8(cell_count * OUT_BYTES_PER_CELL); char *base = (char *)mem + align8((int)sizeof(struct Clayterm));
char *clay_mem = base + cell_bytes * 2 + out_bytes; int clay_bytes = align64((int)Clay_MinMemorySize()); Clay_Arena arena = Clay_CreateArenaWithCapacityAndMemory(clay_bytes, clay_mem); Clay_Initialize(arena, (Clay_Dimensions){(float)w, (float)h}, (Clay_ErrorHandler){clay_error, ct});
*ct = (struct Clayterm){ .w = w, .h = h, .front = (Cell *)base, .back = (Cell *)(base + cell_bytes), .out = {base + cell_bytes * 2, 0, cell_count * OUT_BYTES_PER_CELL}, .lastfg = 0xffffffff, .lastbg = 0xffffffff, .lastx = -1, .lasty = -1, .colors = 256, /* §7.1 baseline until the host pushes evidence */ .default_bg = 0x000000, /* §7.9 fallback: black background */ .default_fg = 0xffffff, /* §7.9 fallback: white foreground */ .last_tier = -1, };
// initialize back buffer with spaces and default fg/bg cells_fill(ct->back, w, h, ' ', ATTR_DEFAULT, ATTR_DEFAULT);
// initialize front buffer with zeros. Every cell will be cells_fill(ct->front, w, h, 0, 0, 0); return ct;}
void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, float deltaTime) { int i = 0; ct_active_context = ct; ct->error_count = 0; ct->animating_count = 0; ct->caret_text_chars = NULL; ct->caret_text_length = 0; ct->caret_offset_bytes = 0; ct->caret_placed = 0; ct->caret_tail_valid = 0; ct->caret_tail_x = -1; ct->caret_tail_y = -1; ct->caret_x = -1; ct->caret_y = -1; ct->has_caret = 0;
Clay_BeginLayout();
while (i < len) { uint32_t op = rd(buf, len, &i);
switch (op) { case OP_OPEN_ELEMENT: { /* read id string */ uint32_t id_len = rd(buf, len, &i); int id_words = (id_len + 3) / 4; char *id_chars = (char *)&buf[i]; i += id_words;
if (id_len > 0) { Clay_String str = {.length = (int32_t)id_len, .chars = id_chars}; Clay_ElementId eid = Clay__HashString(str, 0); Clay__OpenElementWithId(eid); } else { Clay__OpenElement(); }
/* read property mask */ uint32_t mask = rd(buf, len, &i); Clay_ElementDeclaration decl = {0};
if (mask & PROP_LAYOUT) { decl.layout.sizing.width = decode_axis(buf, len, &i); decl.layout.sizing.height = decode_axis(buf, len, &i);
uint32_t pad = rd(buf, len, &i); decl.layout.padding.left = pad & 0xff; decl.layout.padding.right = (pad >> 8) & 0xff; decl.layout.padding.top = (pad >> 16) & 0xff; decl.layout.padding.bottom = (pad >> 24) & 0xff;
uint32_t gd = rd(buf, len, &i); decl.layout.childGap = gd & 0xffff; decl.layout.layoutDirection = (gd >> 16) & 0xff;
uint32_t al = rd(buf, len, &i); decl.layout.childAlignment.x = al & 0xff; decl.layout.childAlignment.y = (al >> 8) & 0xff; }
if (mask & PROP_BG_COLOR) { decl.backgroundColor = unpack_color(rd(buf, len, &i)); }
if (mask & PROP_CORNER_RADIUS) { uint32_t cr = rd(buf, len, &i); decl.cornerRadius.topLeft = (float)(cr & 0xff); decl.cornerRadius.topRight = (float)((cr >> 8) & 0xff); decl.cornerRadius.bottomLeft = (float)((cr >> 16) & 0xff); decl.cornerRadius.bottomRight = (float)((cr >> 24) & 0xff); }
if (mask & PROP_BORDER) { uint32_t bw = rd(buf, len, &i); decl.border.width.left = bw & 0xff; decl.border.width.right = (bw >> 8) & 0xff; decl.border.width.top = (bw >> 16) & 0xff; decl.border.width.bottom = (bw >> 24) & 0xff;
/* Border sides reserve layout space additively: effective padding * is userPadding + borderWidth per side (renderer-spec 12.2). */ decl.layout.padding.left += decl.border.width.left; decl.layout.padding.right += decl.border.width.right; decl.layout.padding.top += decl.border.width.top; decl.layout.padding.bottom += decl.border.width.bottom;
/* Resolved per-side fg/bg attribute words (top, right, bottom, * left). Routed to render_border via userData; the command buffer * remains valid for the whole render pass. */ if (i + 8 <= len) decl.userData = (void *)&buf[i]; i += 8; }
if (mask & PROP_CLIP) { uint32_t cl = rd(buf, len, &i); decl.clip.horizontal = cl & 0xff; decl.clip.vertical = (cl >> 8) & 0xff; }
if (mask & PROP_FLOATING) { decl.floating.offset.x = rdf(buf, len, &i); decl.floating.offset.y = rdf(buf, len, &i); decl.floating.expand.width = rdf(buf, len, &i); decl.floating.expand.height = rdf(buf, len, &i); decl.floating.parentId = rd(buf, len, &i);
uint32_t fc = rd(buf, len, &i); decl.floating.attachTo = fc & 0xff; decl.floating.attachPoints.element = (fc >> 8) & 0xff; decl.floating.attachPoints.parent = (fc >> 16) & 0xff; decl.floating.pointerCaptureMode = (fc >> 24) & 0xff;
uint32_t fd = rd(buf, len, &i); decl.floating.clipTo = fd & 0xff; decl.floating.zIndex = (int16_t)(fd >> 8); }
if (mask & PROP_TRANSITION) { float duration = rdf(buf, len, &i); uint32_t props_and_flags = rd(buf, len, &i); uint16_t props = props_and_flags & 0xFFFF; uint8_t easing = (props_and_flags >> 16) & 0xFF; uint8_t interactive = (props_and_flags >> 24) & 0xFF;
decl.transition.handler = ct_handler_for(easing); decl.transition.duration = duration; decl.transition.properties = (Clay_TransitionProperty)props; decl.transition.interactionHandling = interactive ? CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION : CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION; }
Clay__ConfigureOpenElement(decl); break; }
case OP_TEXT: { uint32_t col = rd(buf, len, &i); /* The [bg, attrs] window starts here; render_text reads both * words through userData. */ int bg_index = i; rd(buf, len, &i); /* bg word: 0xAARRGGBB, alpha 0 = default */ uint32_t attrs = rd(buf, len, &i); uint32_t cfg = rd(buf, len, &i); uint32_t caret = rd(buf, len, &i); uint32_t str_len = rd(buf, len, &i); int str_words = (str_len + 3) / 4; char *str_chars = (char *)&buf[i]; i += str_words;
/* A caret on empty content is a rendering commitment the layout * engine cannot satisfy on its own — zero cells give render_text * nothing to attach the cursor to. Substitute a single space so * the caret lands at the text element's origin, per the spec's * "as if the content were a single space" outcome. */ if (caret != 0xFFFFFFFF && str_len == 0) { str_chars = (char *)" "; str_len = 1; }
/* Record the FIRST caret declaration per frame for the * single-hardware-cursor contract; later declarations are * intentionally ignored (multi-cursor is unspecified). * * Convert the caller's code-point offset into a byte offset once * here so the render walk can identify the caret cell by simple * pointer arithmetic against the slice's chars pointer. */ if (caret != 0xFFFFFFFF && ct->caret_text_chars == NULL) { ct->caret_text_chars = str_chars; ct->caret_text_length = (int)str_len; ct->caret_offset_bytes = utf8_bytes_for_cps(str_chars, caret, str_len); ct->has_caret = 1; }
Clay_String text = {.length = (int32_t)str_len, .chars = str_chars};
Clay_TextElementConfig config = {0}; /* render_text reads the [bg, attrs] window from the command * buffer through this pointer. */ config.userData = (void *)&buf[bg_index]; config.textColor = unpack_color(col); config.fontSize = cfg & 0xff; config.fontId = (cfg >> 8) & 0xff; config.wrapMode = (cfg >> 16) & 0xff;
Clay__OpenTextElement(text, config); break; }
case OP_CLOSE_ELEMENT: Clay__CloseElement(); break;
default: break; } }
Clay_RenderCommandArray cmds = Clay_EndLayout(deltaTime);
/* reset output state */ ct->out.length = 0; ct->lastfg = ct->lastbg = 0xffffffff; ct->lastx = ct->lasty = -1; ct->clipdepth = 0; ct->clipoverflow = 0; ct->clip_depth_exceeded = 0; ct->combining_exceeded = 0; ct->clipping = 0;
cells_fill(ct->back, ct->w, ct->h, ' ', ATTR_DEFAULT, ATTR_DEFAULT);
/* walk Clay render commands into back buffer */ for (int32_t j = 0; j < cmds.length; j++) { Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&cmds, j); Clay_BoundingBox box = cmd->boundingBox; int x0 = (int)box.x; int y0 = (int)box.y; int x1 = (int)(box.x + box.width); int y1 = (int)(box.y + box.height);
switch (cmd->commandType) { case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: render_rect(ct, x0, y0, x1, y1, &cmd->renderData.rectangle); break; case CLAY_RENDER_COMMAND_TYPE_TEXT: render_text(ct, x0, y0, cmd); break; case CLAY_RENDER_COMMAND_TYPE_BORDER: render_border(ct, x0, y0, x1, y1, cmd); break; case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { /* intersect the child box with the current active rect (if any) */ int nx0 = x0, ny0 = y0, nx1 = x1, ny1 = y1; if (ct->clipdepth > 0) { ClipRect top = ct->clipstack[ct->clipdepth - 1]; if (top.x > nx0) nx0 = top.x; if (top.y > ny0) ny0 = top.y; if (top.x + top.w < nx1) nx1 = top.x + top.w; if (top.y + top.h < ny1) ny1 = top.y + top.h; } int nw = nx1 - nx0; int nh = ny1 - ny0; if (nw < 0) nw = 0; if (nh < 0) nh = 0; if (ct->clipdepth < CLIP_STACK_MAX) { ClipRect r = {nx0, ny0, nw, nh}; ct->clipstack[ct->clipdepth++] = r; ct->clipping = 1; ct->clipx = nx0; ct->clipy = ny0; ct->clipw = nw; ct->cliph = nh; } else { /* Out of tracked slots: coalesce this level into the deepest tracked * region (leave the active rect untouched) and remember to pop it * without disturbing the tracked stack. */ ct->clipoverflow++; report_clip_depth_exceeded(ct); } break; } case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: if (ct->clipoverflow > 0) { /* Closing an untracked level: nothing was pushed, so leave the tracked * stack and active rect alone. */ ct->clipoverflow--; break; } if (ct->clipdepth > 0) ct->clipdepth--; if (ct->clipdepth > 0) { ClipRect top = ct->clipstack[ct->clipdepth - 1]; ct->clipping = 1; ct->clipx = top.x; ct->clipy = top.y; ct->clipw = top.w; ct->cliph = top.h; } else { ct->clipping = 0; } break; default: break; } }
/* End-of-content fallback: if a caret was declared and its target byte * offset is one past the last byte of the caret text node, the trailing * cell of the last walked caret slice is where it lands. */ if (ct->has_caret && !ct->caret_placed) { if (ct->caret_tail_valid && ct->caret_offset_bytes == (uint32_t)ct->caret_text_length) { ct->caret_x = ct->caret_tail_x; ct->caret_y = ct->caret_tail_y; ct->caret_placed = 1; } else { /* Offset out of range or otherwise unresolvable. Suppress the * cursor for this frame. */ ct->has_caret = 0; } }
/* Resolve the emission tier for this frame and force a complete * redraw when it changed (color-encoding-spec CI-5): diff state is * cell-based, so unchanged cells would otherwise keep bytes from the * previous encoding on screen. Zeroing the front buffer makes every * cell diff as changed. */ ct->tier = resolve_tier(ct); if (ct->tier != ct->last_tier) { cells_fill(ct->front, ct->w, ct->h, 0, 0, 0); ct->last_tier = ct->tier; }
if (mode == 1) { present_lines(ct); } else { present_cups(ct, row); }
ct_active_context = NULL;}
char *output(struct Clayterm *ct) { return ct->out.data; }
int length(struct Clayterm *ct) { return ct->out.length; }
int animating(struct Clayterm *ct) { return ct->animating_count; }
int get_element_bounds(const char *name, int name_len, float *out) { Clay_String str = {.length = name_len, .chars = name}; Clay_ElementId eid = Clay__HashString(str, 0); Clay_ElementData data = Clay_GetElementData(eid); if (!data.found) { return 0; } out[0] = data.boundingBox.x; out[1] = data.boundingBox.y; out[2] = data.boundingBox.width; out[3] = data.boundingBox.height; return 1;}
int pointer_over_count(void) { return Clay_GetPointerOverIds().length; }
int pointer_over_id_string_length(int index) { Clay_ElementIdArray ids = Clay_GetPointerOverIds(); if (index >= ids.length) return 0; return ids.internalArray[index].stringId.length;}
int pointer_over_id_string_ptr(int index) { Clay_ElementIdArray ids = Clay_GetPointerOverIds(); if (index >= ids.length) return 0; return (int)ids.internalArray[index].stringId.chars;}
void measure(int ret, int txt) { /* Read Clay_StringSlice from txt address. * Clay_StringSlice layout: { int32_t length, const char *chars, ... } * We only need length and chars. */ int32_t slen = *(int32_t *)txt; const char *chars = *(const char **)(txt + 4);
int w = 0; const char *p = chars; int rem = slen; while (rem > 0) { uint32_t cp; int n = utf8_decode(&cp, p); if (n <= 0) { n = 1; cp = 0xfffd; } /* Mirror draw_text: non-printables render as one U+FFFD cell, so * they must measure as one cell. */ int cw = wcwidth(cp); if (cw < 0) cw = 1; if (cw > 0) w += cw; p += n; rem -= n; }
/* Write Clay_Dimensions { float width, float height } to ret */ float *dims = (float *)ret; dims[0] = (float)w; dims[1] = 1.0f;}