Created
July 3, 2026 20:29
-
-
Save erik-nilcoast/4f082288dd32f85f06e541423ffb2c70 to your computer and use it in GitHub Desktop.
boxc - single-header C99 box-model layout engine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #pragma once | |
| // A minimal, allocation-free box model: one flex axis per box, nestable for | |
| // 2D layouts (a row of columns, a column of rows, ...). Callers own all Box | |
| // memory -- static, stack, or embedded in a larger struct -- box_layout() | |
| // only reads the sizing fields below and writes x/y/w/h back. | |
| // | |
| // Single-header: this file is always just declarations. In exactly one C | |
| // file, #define BOX_IMPLEMENTATION before #include "box.h" to pull in the | |
| // implementation -- defining it in more than one translation unit is a | |
| // duplicate-symbol link error, same as any other stb-style header. | |
| typedef enum { BOX_ROW, BOX_COLUMN } BoxDir; | |
| typedef enum { BOX_START, BOX_CENTER, BOX_END, BOX_SPACE_BETWEEN } BoxAlign; | |
| typedef struct Box { | |
| // input: size along the PARENT's main axis. A fixed box claims exactly | |
| // `fixed` px; a flexible box (fixed == 0) shares leftover space by `grow`. | |
| int fixed; | |
| int grow; | |
| // input: size along the parent's cross axis. 0 stretches to fill it. | |
| int cross; | |
| // input: this box's own layout, applied to its children. | |
| BoxDir dir; | |
| BoxAlign align_main, align_cross; // align_cross ignores SPACE_BETWEEN | |
| int padding, gap; | |
| struct Box *children; | |
| int n_children; | |
| // output: filled in by box_layout(). | |
| int x, y, w, h; | |
| } Box; | |
| // Resolves x/y/w/h for `root` and its whole subtree, given the rect `root` | |
| // must fit into. | |
| void box_layout(Box *root, int x, int y, int w, int h); | |
| #ifdef BOX_IMPLEMENTATION | |
| #include <stdbool.h> | |
| // A child's resolved main-axis size is a pure function of its own fixed/grow | |
| // fields plus the parent's leftover/grow_total -- recomputing it here instead | |
| // of caching it in a temp array avoids needing an allocation or a fixed cap | |
| // on child count. | |
| static int child_main(const Box *c, int leftover, int grow_total) { | |
| if (c->fixed > 0) return c->fixed; | |
| return grow_total > 0 ? leftover * c->grow / grow_total : 0; | |
| } | |
| static void layout_children(Box *b) { | |
| bool row = b->dir == BOX_ROW; | |
| int main = row ? b->w : b->h; | |
| int cross = row ? b->h : b->w; | |
| int gaps = b->n_children > 1 ? b->n_children - 1 : 0; | |
| int avail = main - 2 * b->padding - b->gap * gaps; | |
| int fixed_total = 0, grow_total = 0; | |
| for (int i = 0; i < b->n_children; i++) { | |
| Box *c = &b->children[i]; | |
| if (c->fixed > 0) | |
| fixed_total += c->fixed; | |
| else | |
| grow_total += c->grow; | |
| } | |
| int leftover = avail - fixed_total; | |
| if (leftover < 0) leftover = 0; | |
| int used = 0; | |
| for (int i = 0; i < b->n_children; i++) used += child_main(&b->children[i], leftover, grow_total); | |
| int gap = b->gap; | |
| int pos = b->padding; | |
| int slack = avail - used - gap * gaps; | |
| if (slack < 0) slack = 0; | |
| switch (b->align_main) { | |
| case BOX_CENTER: pos += slack / 2; break; | |
| case BOX_END: pos += slack; break; | |
| case BOX_SPACE_BETWEEN: | |
| if (gaps > 0) gap += slack / gaps; | |
| break; | |
| case BOX_START: break; | |
| } | |
| for (int i = 0; i < b->n_children; i++) { | |
| Box *c = &b->children[i]; | |
| int size = child_main(c, leftover, grow_total); | |
| int cross_size = c->cross > 0 ? c->cross : cross - 2 * b->padding; | |
| int cross_pos = b->padding; | |
| if (c->cross > 0) { | |
| int cross_slack = cross - 2 * b->padding - c->cross; | |
| if (cross_slack < 0) cross_slack = 0; | |
| switch (b->align_cross) { | |
| case BOX_CENTER: cross_pos += cross_slack / 2; break; | |
| case BOX_END: cross_pos += cross_slack; break; | |
| default: break; // START, SPACE_BETWEEN (not meaningful cross-axis) | |
| } | |
| } | |
| int cx = b->x + (row ? pos : cross_pos); | |
| int cy = b->y + (row ? cross_pos : pos); | |
| int cw = row ? size : cross_size; | |
| int ch = row ? cross_size : size; | |
| box_layout(c, cx, cy, cw, ch); | |
| pos += size + gap; | |
| } | |
| } | |
| void box_layout(Box *b, int x, int y, int w, int h) { | |
| b->x = x; | |
| b->y = y; | |
| b->w = w; | |
| b->h = h; | |
| if (b->n_children > 0) layout_children(b); | |
| } | |
| #endif // BOX_IMPLEMENTATION |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment