aboutsummaryrefslogtreecommitdiff
path: root/core.c
diff options
context:
space:
mode:
Diffstat (limited to 'core.c')
-rw-r--r--core.c755
1 files changed, 755 insertions, 0 deletions
diff --git a/core.c b/core.c
new file mode 100644
index 0000000..aa72e8e
--- /dev/null
+++ b/core.c
@@ -0,0 +1,755 @@
+/* core.c - the interaction state machine, independent of any backend. */
+#include "core.h"
+#include "draw.h"
+#include "font.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define MAX_LABEL 24
+
+struct rect { int x, y, w, h; };
+
+struct cell {
+ struct rect r; /* global coordinates */
+ int screen;
+ char label[MAX_LABEL];
+};
+
+enum action { ACT_NONE, ACT_LEFT, ACT_MIDDLE, ACT_RIGHT, ACT_DOUBLE, ACT_DRAG };
+
+/* ---- labels ---------------------------------------------------------- */
+
+static int label_len(int base, int n)
+{
+ int len = 1;
+ long cap = base;
+ while (cap < n) {
+ cap *= base;
+ len++;
+ }
+ return len;
+}
+
+static void make_label(char *out, const char *alpha, int base, int len, int idx)
+{
+ out[len] = 0;
+ for (int i = len - 1; i >= 0; i--) {
+ out[i] = alpha[idx % base];
+ idx /= base;
+ }
+}
+
+/* ---- geometry -------------------------------------------------------- */
+
+static int fit_scale(int cw, int ch, int len)
+{
+ int wunit = len * FONT_W + (len - 1); /* label width at scale 1 */
+ int sw = (cw * 8 / 10) / wunit; /* fit 80% of cell width */
+ int sh = (ch * 6 / 10) / FONT_H; /* fit 60% of cell height */
+ int s = sw < sh ? sw : sh;
+ if (s < 1)
+ s = 1;
+ if (s > 8)
+ s = 8;
+ return s;
+}
+
+static int screen_at(struct platform *p, int x, int y)
+{
+ for (int s = 0; s < p->n_screens; s++) {
+ struct mb_screen c = p->screens[s];
+ if (x >= c.x && x < c.x + c.w && y >= c.y && y < c.y + c.h)
+ return s;
+ }
+ return -1;
+}
+
+static int build_cells(const struct config *cfg, struct platform *p,
+ struct cell **out, int *out_len)
+{
+ int base = (int)strlen(cfg->alphabet);
+ int total = 0;
+ int cols[MB_MAX_SCREENS], rows[MB_MAX_SCREENS];
+
+ for (int s = 0; s < p->n_screens; s++) {
+ struct mb_screen sc = p->screens[s];
+ int c, r;
+ if (cfg->cols > 0 && cfg->rows > 0) {
+ c = cfg->cols;
+ r = cfg->rows;
+ } else {
+ int cs = cfg->cell_size > 0 ? cfg->cell_size : 100;
+ c = (sc.w + cs / 2) / cs;
+ r = (sc.h + cs / 2) / cs;
+ }
+ if (c < 1)
+ c = 1;
+ if (r < 1)
+ r = 1;
+ if (c > MB_MAX_GRID_DIM)
+ c = MB_MAX_GRID_DIM;
+ if (r > MB_MAX_GRID_DIM)
+ r = MB_MAX_GRID_DIM;
+ cols[s] = c;
+ rows[s] = r;
+ total += c * r;
+ }
+ if (total <= 0)
+ return 0;
+
+ int len = label_len(base, total);
+ if (len >= MAX_LABEL)
+ len = MAX_LABEL - 1;
+
+ struct cell *cells = calloc((size_t)total, sizeof *cells);
+ if (!cells)
+ return 0;
+
+ int idx = 0;
+ for (int s = 0; s < p->n_screens; s++) {
+ struct mb_screen sc = p->screens[s];
+ for (int cy = 0; cy < rows[s]; cy++) {
+ for (int cx = 0; cx < cols[s]; cx++) {
+ int x0 = sc.x + cx * sc.w / cols[s];
+ int x1 = sc.x + (cx + 1) * sc.w / cols[s];
+ int y0 = sc.y + cy * sc.h / rows[s];
+ int y1 = sc.y + (cy + 1) * sc.h / rows[s];
+ struct cell *cl = &cells[idx];
+ cl->r = (struct rect){ x0, y0, x1 - x0,
+ y1 - y0 };
+ cl->screen = s;
+ make_label(cl->label, cfg->alphabet, base, len,
+ idx);
+ idx++;
+ }
+ }
+ }
+
+ *out = cells;
+ *out_len = len;
+ return total;
+}
+
+/* ---- rendering ------------------------------------------------------- */
+
+/* Begin every screen's overlay and paint the dim backdrop, filling bufs[]. */
+static int frame_begin_dim(struct platform *p, const struct config *cfg,
+ struct mb_buffer **bufs)
+{
+ for (int s = 0; s < p->n_screens; s++) {
+ bufs[s] = p->frame_begin(p, s);
+ if (!bufs[s])
+ return -1;
+ draw_fill(bufs[s], 0, 0, bufs[s]->w, bufs[s]->h, cfg->color_bg);
+ }
+ return 0;
+}
+
+static int render_grid(struct platform *p, const struct config *cfg,
+ struct cell *cells, int ncells, int len,
+ const char *typed)
+{
+ struct mb_buffer *bufs[MB_MAX_SCREENS];
+ if (frame_begin_dim(p, cfg, bufs))
+ return -1;
+
+ int tlen = (int)strlen(typed);
+ for (int i = 0; i < ncells; i++) {
+ struct cell *cl = &cells[i];
+ struct mb_buffer *b = bufs[cl->screen];
+ struct mb_screen sc = p->screens[cl->screen];
+ int lx = cl->r.x - sc.x, ly = cl->r.y - sc.y;
+
+ draw_rect(b, lx, ly, cl->r.w, cl->r.h, 1, cfg->color_grid);
+ if (tlen && strncmp(cl->label, typed, (size_t)tlen) != 0)
+ continue;
+
+ int scale = cfg->font_scale > 0 ? cfg->font_scale
+ : fit_scale(cl->r.w, cl->r.h, len);
+ const char *lab = cl->label;
+ int tx = lx + (cl->r.w - text_width(lab, scale)) / 2;
+ int ty = ly + (cl->r.h - text_height(scale)) / 2;
+
+ if (tlen) {
+ char pre[MAX_LABEL];
+ memcpy(pre, lab, (size_t)tlen);
+ pre[tlen] = 0;
+ draw_text(b, tx, ty, pre, scale, cfg->color_typed);
+ int off = text_width(pre, scale) + scale;
+ draw_text(b, tx + off, ty, lab + tlen, scale,
+ cfg->color_label);
+ } else {
+ draw_text(b, tx, ty, lab, scale, cfg->color_label);
+ }
+ }
+ return p->frame_commit(p);
+}
+
+/* Draw a refine key centred in its quadrant, while the quadrant is large enough
+ * for the glyph to be legible; it shrinks away as the region is bisected. */
+static void draw_quad_key(struct mb_buffer *b, int qx, int qy, int qw, int qh,
+ char key, uint32_t fg)
+{
+ int smin = qw < qh ? qw : qh;
+ int scale = (smin / 2) / FONT_W;
+ if (scale < 1)
+ return;
+ if (scale > 6)
+ scale = 6;
+ char s[2] = { key, 0 };
+ int tx = qx + (qw - text_width(s, scale)) / 2;
+ int ty = qy + (qh - text_height(scale)) / 2;
+ draw_text(b, tx, ty, s, scale, fg);
+}
+
+static int render_refine(struct platform *p, const struct config *cfg,
+ struct rect cur)
+{
+ struct mb_buffer *bufs[MB_MAX_SCREENS];
+ if (frame_begin_dim(p, cfg, bufs))
+ return -1;
+
+ int cx = cur.x + cur.w / 2, cy = cur.y + cur.h / 2;
+ int s = screen_at(p, cx, cy);
+ if (s < 0)
+ s = 0;
+ struct mb_buffer *b = bufs[s];
+ struct mb_screen sc = p->screens[s];
+ int lx = cur.x - sc.x, ly = cur.y - sc.y;
+ uint32_t ac = cfg->color_active;
+ /* Guide lines at ~38% of the active alpha, so they track opacity. */
+ uint32_t guide = (ac & 0x00ffffff) | (((ac >> 24) * 0x60 / 0xff) << 24);
+
+ draw_rect(b, lx, ly, cur.w, cur.h, 2, ac);
+ draw_vline(b, lx + cur.w / 2, ly, cur.h, 1, guide);
+ draw_hline(b, lx, ly + cur.h / 2, cur.w, 1, guide);
+
+ int hw = cur.w / 2, hh = cur.h / 2;
+ int rw = cur.w - hw, rh = cur.h - hh;
+ draw_quad_key(b, lx, ly, hw, hh, cfg->k_tl, cfg->color_label);
+ draw_quad_key(b, lx + hw, ly, rw, hh, cfg->k_tr, cfg->color_label);
+ draw_quad_key(b, lx, ly + hh, hw, rh, cfg->k_bl, cfg->color_label);
+ draw_quad_key(b, lx + hw, ly + hh, rw, rh, cfg->k_br, cfg->color_label);
+
+ int gx = lx + cur.w / 2, gy = ly + cur.h / 2;
+ draw_hline(b, gx - 12, gy - 1, 25, 2, ac);
+ draw_vline(b, gx - 1, gy - 12, 25, 2, ac);
+ return p->frame_commit(p);
+}
+
+/* ---- actions --------------------------------------------------------- */
+
+static enum action action_for(const struct config *cfg, uint32_t k)
+{
+ if (k == MB_KEY_ENTER)
+ return ACT_LEFT;
+ if (k > 0x7e)
+ return ACT_NONE;
+ char c = (char)k;
+ if (c == cfg->k_left)
+ return ACT_LEFT;
+ if (c == cfg->k_middle)
+ return ACT_MIDDLE;
+ if (c == cfg->k_right)
+ return ACT_RIGHT;
+ if (c == cfg->k_double)
+ return ACT_DOUBLE;
+ if (c == cfg->k_drag)
+ return ACT_DRAG;
+ return ACT_NONE;
+}
+
+#define CLICK_MS 12 /* down->up and inter-click gap; 0ms clicks get missed */
+
+static int do_click(struct platform *p, enum mb_button b, int times)
+{
+ for (int i = 0; i < times; i++) {
+ if (i)
+ platform_sleep_ms(CLICK_MS);
+ if (p->pointer_button(p, b, 1)) {
+ p->pointer_button(p, b, 0);
+ return -1;
+ }
+ platform_sleep_ms(CLICK_MS);
+ if (p->pointer_button(p, b, 0))
+ return -1;
+ }
+ return 0;
+}
+
+static int emit(struct platform *p, int dry, int x, int y, enum action a)
+{
+ static const char *name[] = { "none", "left", "middle",
+ "right", "double", "drag" };
+ if (dry) {
+ printf("%d %d %s\n", x, y, name[a]);
+ return 0;
+ }
+ if (p->pointer_move(p, x, y))
+ return -1;
+ switch (a) {
+ case ACT_LEFT:
+ return do_click(p, MB_LEFT, 1);
+ case ACT_MIDDLE:
+ return do_click(p, MB_MIDDLE, 1);
+ case ACT_RIGHT:
+ return do_click(p, MB_RIGHT, 1);
+ case ACT_DOUBLE:
+ return do_click(p, MB_LEFT, 2);
+ default:
+ return 0;
+ }
+}
+
+/* ---- main flow ------------------------------------------------------- */
+
+int core_run(struct platform *p, const struct config *cfg, int dry)
+{
+ if (cfg->cols > 0 && cfg->rows > 0) {
+ for (int s = 0; s < p->n_screens; s++) {
+ if (cfg->cols > p->screens[s].w ||
+ cfg->rows > p->screens[s].h) {
+ fprintf(stderr,
+ "mouseboard: grid exceeds screen dimensions\n");
+ return 2;
+ }
+ }
+ }
+ struct cell *cells;
+ int len;
+ int ncells = build_cells(cfg, p, &cells, &len);
+ if (ncells <= 0) {
+ fprintf(stderr, "mouseboard: no screen cells\n");
+ return 2;
+ }
+
+ enum { GRID, REFINE } state = GRID;
+ char typed[MAX_LABEL] = "";
+ int tn = 0;
+ struct rect stack[64];
+ int sp = 0;
+ int drag = 0;
+ int rc = 2;
+
+ if (render_grid(p, cfg, cells, ncells, len, typed))
+ goto out;
+
+ for (;;) {
+ uint32_t k = p->next_key(p);
+ if (k >= 'A' && k <= 'Z')
+ k += 'a' - 'A'; /* labels and bindings are lowercase;
+ makes Caps Lock harmless */
+ if (k == MB_KEY_NONE) {
+ rc = 2;
+ break;
+ }
+ if (k == MB_KEY_ESC) {
+ rc = 1;
+ break;
+ }
+
+ if (state == GRID) {
+ if (k == MB_KEY_BACKSPACE) {
+ if (tn) {
+ typed[--tn] = 0;
+ if (render_grid(p, cfg, cells, ncells, len,
+ typed))
+ break;
+ }
+ continue;
+ }
+ if (k < 0x20 || k > 0x7e || tn >= MAX_LABEL - 1)
+ continue;
+
+ typed[tn] = (char)k;
+ typed[tn + 1] = 0;
+ int matches = 0, exact = -1;
+ for (int i = 0; i < ncells; i++) {
+ if (strncmp(cells[i].label, typed,
+ (size_t)(tn + 1)))
+ continue;
+ matches++;
+ if ((int)strlen(cells[i].label) == tn + 1)
+ exact = i;
+ }
+ if (matches == 0) {
+ typed[tn] = 0; /* reject, keep prompt */
+ continue;
+ }
+ tn++;
+ if (exact >= 0 && tn == len) {
+ stack[0] = cells[exact].r;
+ sp = 1;
+ state = REFINE;
+ struct rect c = stack[0];
+ if (!dry) {
+ if (p->pointer_move(p, c.x + c.w / 2,
+ c.y + c.h / 2))
+ break;
+ }
+ if (render_refine(p, cfg, c))
+ break;
+ } else {
+ if (render_grid(p, cfg, cells, ncells, len, typed))
+ break;
+ }
+ continue;
+ }
+
+ /* REFINE */
+ struct rect cur = stack[sp - 1];
+ if (k == MB_KEY_BACKSPACE) {
+ if (sp > 1) {
+ sp--;
+ cur = stack[sp - 1];
+ if (!dry) {
+ if (p->pointer_move(p, cur.x + cur.w / 2,
+ cur.y + cur.h / 2))
+ break;
+ }
+ if (render_refine(p, cfg, cur))
+ break;
+ } else {
+ state = GRID;
+ tn = 0;
+ typed[0] = 0;
+ if (render_grid(p, cfg, cells, ncells, len, typed))
+ break;
+ }
+ continue;
+ }
+
+ char c = (k <= 0x7e) ? (char)k : 0;
+ int hw = cur.w / 2, hh = cur.h / 2;
+ int rw = cur.w - hw, rh = cur.h - hh;
+ struct rect nr;
+ int split = 1;
+ if (c && c == cfg->k_tl)
+ nr = (struct rect){ cur.x, cur.y, hw, hh };
+ else if (c && c == cfg->k_tr)
+ nr = (struct rect){ cur.x + hw, cur.y, rw, hh };
+ else if (c && c == cfg->k_bl)
+ nr = (struct rect){ cur.x, cur.y + hh, hw, rh };
+ else if (c && c == cfg->k_br)
+ nr = (struct rect){ cur.x + hw, cur.y + hh, rw, rh };
+ else
+ split = 0;
+
+ if (split) {
+ if (sp >= (int)(sizeof stack / sizeof stack[0]))
+ continue; /* depth limit; the region has been
+ 1x1 for dozens of splits by now */
+ if (nr.w < 1)
+ nr.w = 1;
+ if (nr.h < 1)
+ nr.h = 1;
+ stack[sp++] = nr;
+ cur = nr;
+ if (!dry) {
+ if (p->pointer_move(p, cur.x + cur.w / 2,
+ cur.y + cur.h / 2))
+ break;
+ }
+ if (render_refine(p, cfg, cur))
+ break;
+ continue;
+ }
+
+ enum action a = action_for(cfg, k);
+ if (a == ACT_NONE)
+ continue;
+
+ int tx = cur.x + cur.w / 2, ty = cur.y + cur.h / 2;
+ if (a == ACT_DRAG && !drag) {
+ if (dry)
+ printf("%d %d drag-start\n", tx, ty);
+ else {
+ if (p->pointer_move(p, tx, ty))
+ break;
+ if (p->pointer_button(p, MB_LEFT, 1)) {
+ p->pointer_button(p, MB_LEFT, 0);
+ break;
+ }
+ }
+ drag = 1;
+ state = GRID;
+ tn = 0;
+ typed[0] = 0;
+ if (render_grid(p, cfg, cells, ncells, len, typed))
+ break;
+ continue;
+ }
+ if (drag) {
+ if (dry)
+ printf("%d %d drag-end\n", tx, ty);
+ else {
+ if (p->pointer_move(p, tx, ty) ||
+ p->pointer_button(p, MB_LEFT, 0))
+ break;
+ }
+ drag = 0;
+ rc = 0;
+ break;
+ }
+ if (emit(p, dry, tx, ty, a))
+ break;
+ rc = 0;
+ break;
+ }
+
+out:
+ if (drag && !dry && p->pointer_button(p, MB_LEFT, 0))
+ rc = 2;
+ free(cells);
+ return rc;
+}
+
+/* ---- offline self-test ---------------------------------------------- */
+
+static int st_fail;
+
+static void check(int cond, const char *what)
+{
+ if (!cond) {
+ st_fail++;
+ printf("FAIL: %s\n", what);
+ } else {
+ printf("ok: %s\n", what);
+ }
+}
+
+struct test_platform {
+ uint32_t keys[8];
+ int key_count;
+ int key_pos;
+ int commits;
+ int moves;
+ int buttons;
+ int last_x, last_y;
+ int fail_commit;
+ int fail_move;
+ int fail_button_down;
+ struct mb_buffer buffer;
+ uint32_t pixels[100 * 80];
+};
+
+static struct mb_buffer *test_frame_begin(struct platform *p, int screen)
+{
+ (void)screen;
+ return &((struct test_platform *)p->priv)->buffer;
+}
+
+static int test_frame_commit(struct platform *p)
+{
+ struct test_platform *t = p->priv;
+ t->commits++;
+ return t->fail_commit ? -1 : 0;
+}
+
+static uint32_t test_next_key(struct platform *p)
+{
+ struct test_platform *t = p->priv;
+ if (t->key_pos >= t->key_count)
+ return MB_KEY_NONE;
+ return t->keys[t->key_pos++];
+}
+
+static int test_pointer_move(struct platform *p, int x, int y)
+{
+ struct test_platform *t = p->priv;
+ t->moves++;
+ t->last_x = x;
+ t->last_y = y;
+ return t->fail_move ? -1 : 0;
+}
+
+static int test_pointer_button(struct platform *p, enum mb_button b, int press)
+{
+ (void)b;
+ struct test_platform *t = p->priv;
+ t->buttons++;
+ if (press && t->fail_button_down)
+ return -1;
+ return 0;
+}
+
+static void test_platform_init(struct platform *p, struct test_platform *t)
+{
+ memset(t, 0, sizeof *t);
+ *p = (struct platform){ 0 };
+ p->n_screens = 1;
+ p->screens[0] = (struct mb_screen){ 0, 0, 100, 80 };
+ p->frame_begin = test_frame_begin;
+ p->frame_commit = test_frame_commit;
+ p->next_key = test_next_key;
+ p->pointer_move = test_pointer_move;
+ p->pointer_button = test_pointer_button;
+ p->priv = t;
+ t->buffer = (struct mb_buffer){ t->pixels, 100, 80, 100, 1 };
+}
+
+int core_selftest(void)
+{
+ st_fail = 0;
+
+ /* label length and base conversion */
+ check(label_len(2, 4) == 2, "label_len(2,4)==2");
+ check(label_len(26, 26) == 1, "label_len(26,26)==1");
+ check(label_len(26, 27) == 2, "label_len(26,27)==2");
+
+ const char *alpha = "abcdef";
+ int base = 6, len = label_len(base, 36);
+ check(len == 2, "len for 36 over base 6 == 2");
+ char l0[8], l1[8], l35[8];
+ make_label(l0, alpha, base, len, 0);
+ make_label(l1, alpha, base, len, 1);
+ make_label(l35, alpha, base, len, 35);
+ check(!strcmp(l0, "aa"), "label 0 == aa");
+ check(!strcmp(l1, "ab"), "label 1 == ab");
+ check(!strcmp(l35, "ff"), "label 35 == ff");
+
+ /* labels are unique across a range */
+ int dup = 0;
+ char seen[36][8];
+ for (int i = 0; i < 36; i++)
+ make_label(seen[i], alpha, base, len, i);
+ for (int i = 0; i < 36 && !dup; i++)
+ for (int j = i + 1; j < 36; j++)
+ if (!strcmp(seen[i], seen[j]))
+ dup = 1;
+ check(!dup, "labels unique over full range");
+
+ /* bisection converges and stays inside the cell */
+ struct rect r = { 0, 0, 1000, 800 };
+ struct rect cur = r;
+ for (int i = 0; i < 12; i++) {
+ int hw = cur.w / 2, hh = cur.h / 2;
+ cur = (struct rect){ cur.x, cur.y, hw, hh }; /* keep TL */
+ }
+ check(cur.w <= 1 && cur.h <= 1, "bisection shrinks to a point");
+ check(cur.x >= r.x && cur.y >= r.y, "bisection stays inside cell");
+
+ /* config parsing */
+ struct config cfg;
+ config_defaults(&cfg);
+ check(config_set(&cfg, "color_active", "#112233") == 0,
+ "parse #RRGGBB");
+ check(cfg.color_active == 0xff112233, "color #RRGGBB -> 0xAARRGGBB");
+ check(config_set(&cfg, "color_bg", "#11223344") == 0,
+ "parse #RRGGBBAA");
+ check(cfg.color_bg == 0x44112233, "color #RRGGBBAA -> 0xAARRGGBB");
+ check(config_set(&cfg, "click_left", "z") == 0, "parse binding");
+ check(cfg.k_left == 'z', "binding applied");
+ check(config_set(&cfg, "bogus", "x") == -1, "unknown key rejected");
+ check(config_set(&cfg, "cell_size", "abc") == -2, "bad int rejected");
+ check(config_set(&cfg, "click_left", "Z") == -2,
+ "uppercase binding rejected");
+ check(config_set(&cfg, "alphabet", "abC") == -2,
+ "uppercase alphabet rejected");
+ check(config_set(&cfg, "opacity_bg", "50") == 0, "parse opacity_bg");
+ check(config_set(&cfg, "opacity_fg", "150") == -2,
+ "opacity >100 rejected");
+ check(config_validate(&cfg) == NULL, "default config is valid");
+ struct config bad = cfg;
+ bad.rows = 2;
+ check(config_validate(&bad) != NULL, "incomplete grid rejected");
+ bad = cfg;
+ bad.k_drag = bad.k_tl;
+ check(config_validate(&bad) != NULL, "binding collision rejected");
+ bad = cfg;
+ bad.font_scale = MB_MAX_FONT_SCALE + 1;
+ check(config_validate(&bad) != NULL, "excessive font scale rejected");
+
+ /* opacity_bg drives the backdrop, opacity_fg everything else; the
+ full-alpha default colours make 100 mean solid */
+ struct config oc;
+ config_defaults(&oc);
+ config_apply_opacity(&oc);
+ check((oc.color_bg >> 24) == 0xff * 33 / 100,
+ "opacity_bg scales backdrop alpha");
+ check((oc.color_label >> 24) == 0xff * 65 / 100,
+ "opacity_fg scales label alpha");
+ struct config os;
+ config_defaults(&os);
+ os.opacity_bg = 100;
+ config_apply_opacity(&os);
+ check((os.color_bg >> 24) == 0xff, "opacity_bg 100 is solid");
+
+ /* build_cells tiles every screen with uniquely labelled cells */
+ struct platform pl = { 0 };
+ pl.n_screens = 2;
+ pl.screens[0] = (struct mb_screen){ 0, 0, 1000, 800 };
+ pl.screens[1] = (struct mb_screen){ 1000, 0, 1920, 1080 };
+ struct config c2;
+ config_defaults(&c2);
+ struct cell *cells;
+ int clen;
+ int n = build_cells(&c2, &pl, &cells, &clen);
+ check(n > 0, "build_cells returns cells");
+ long area[2] = { 0, 0 };
+ int inside = 1, unique = 1;
+ for (int i = 0; i < n; i++) {
+ struct mb_screen sc = pl.screens[cells[i].screen];
+ struct rect cr = cells[i].r;
+ if (cr.x < sc.x || cr.y < sc.y || cr.x + cr.w > sc.x + sc.w ||
+ cr.y + cr.h > sc.y + sc.h)
+ inside = 0;
+ area[cells[i].screen] += (long)cr.w * cr.h;
+ for (int j = i + 1; j < n && unique; j++)
+ if (!strcmp(cells[i].label, cells[j].label))
+ unique = 0;
+ }
+ check(inside, "cells stay inside their screen");
+ check(area[0] == 1000L * 800 && area[1] == 1920L * 1080,
+ "cells tile each screen exactly");
+ check(unique, "cell labels unique across screens");
+ check(n > 0 && (int)strlen(cells[0].label) == clen,
+ "label length matches build_cells");
+ free(cells);
+
+ /* Complete state-machine flows through a fake dependency-free backend. */
+ struct platform tp;
+ struct test_platform ts;
+ struct config tc;
+ config_defaults(&tc);
+ tc.cols = tc.rows = 1;
+ test_platform_init(&tp, &ts);
+ ts.keys[0] = (uint32_t)tc.alphabet[0];
+ ts.keys[1] = MB_KEY_ENTER;
+ ts.key_count = 2;
+ check(core_run(&tp, &tc, 0) == 0, "core click flow succeeds");
+ check(ts.moves == 2 && ts.buttons == 2, "click emits move, down and up");
+ check(ts.last_x == 50 && ts.last_y == 40, "click targets cell centre");
+
+ test_platform_init(&tp, &ts);
+ ts.keys[0] = (uint32_t)tc.alphabet[0];
+ ts.keys[1] = (uint32_t)tc.k_drag;
+ ts.keys[2] = MB_KEY_ESC;
+ ts.key_count = 3;
+ check(core_run(&tp, &tc, 0) == 1, "drag cancellation reports cancel");
+ check(ts.buttons == 2, "drag cancellation releases held button");
+
+ test_platform_init(&tp, &ts);
+ ts.keys[0] = (uint32_t)tc.alphabet[0];
+ ts.key_count = 1;
+ ts.fail_move = 1;
+ check(core_run(&tp, &tc, 0) == 2, "pointer failure reaches caller");
+ check(ts.buttons == 0, "pointer failure emits no click");
+
+ test_platform_init(&tp, &ts);
+ ts.fail_commit = 1;
+ check(core_run(&tp, &tc, 0) == 2, "presentation failure reaches caller");
+
+ test_platform_init(&tp, &ts);
+ ts.keys[0] = (uint32_t)tc.alphabet[0];
+ ts.keys[1] = MB_KEY_ENTER;
+ ts.key_count = 2;
+ ts.fail_button_down = 1;
+ check(core_run(&tp, &tc, 0) == 2, "button failure reaches caller");
+ check(ts.buttons == 2, "failed button press gets release attempt");
+
+ printf(st_fail ? "\n%d FAILED\n" : "\nall passed\n", st_fail);
+ return st_fail ? 1 : 0;
+}