/* core.c - the interaction state machine, independent of any backend. */ #include "core.h" #include "draw.h" #include "font.h" #include #include #include #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; }