diff options
| author | Lena <lena@omega> | 2026-01-01 00:00:00 +0000 |
|---|---|---|
| committer | Lena <lena@omega> | 2026-01-01 00:00:00 +0000 |
| commit | 2fa72ee0dc67d973330aec4ba98260e9f57f7821 (patch) | |
| tree | e6a25c07c8da61d29e4e1f8a72436565e63c5777 /core.c | |
| download | mouseboard-master.tar.gz | |
Keyboard-only pointing: a labelled grid over every monitor, type a
label to jump to a cell, bisect it by quadrant until the crosshair is
exact, then one key clicks. The coarse grid keeps labels short and
bisection keeps precision unlimited, so a hotkey and a few keystrokes
replace the mouse.
The core talks only to the backend interface in platform.h; wlroots
Wayland and Win32 backends supply the overlay, keyboard grab and
pointer.
Diffstat (limited to 'core.c')
| -rw-r--r-- | core.c | 755 |
1 files changed, 755 insertions, 0 deletions
@@ -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; +} |