joeylib2/examples/keys/keys.c
2026-10-19 15:46:47 -05:00

501 lines
18 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Visual keyboard + mouse demo: one square per jlKeyE, lit when its
// key is held, and a small pointer drawn at the live mouse position.
// Holding the left mouse button while the pointer is over a cell lights
// it as if the corresponding key were down. Press ESC to quit.
//
// The strip under the grid is a typed-text line fed by jlInputGetChar:
// whatever you type -- letters, digits, shifted punctuation -- appears
// there, Backspace deletes, Return clears. It doubles as the
// acceptance test for the typed-character queue ("192.168.1.10:6510"
// must be typeable on every port). The bottom two scanlines
// additionally encode verification data as raw pixel nibbles behind
// sentinels: row 198 carries every received character (A5A5 sentinel,
// 2 pixels = 1 byte) and row 199 the live mouse state (5A5A sentinel,
// present/button flags plus position), so emulator harnesses can
// verify the exact bytes from a framebuffer dump.
//
// The render loop only redraws cells whose target lit state changed
// since last frame -- so on idle frames, the only work is the cursor
// erase + redraw + a tiny rect-present pair. The cursor erase is
// implemented by redrawing the cell that contained the *previous*
// cursor position; in the gap regions between cells the cursor will
// briefly leave a trail until that cell is touched again, which is an
// acceptable demo-quality compromise.
#include <stdio.h>
#include <joey/joey.h>
#include <joey/debug.h>
#include "exampleFont.h"
#define GRID_COLS 10
#define GRID_ROWS 6
#define CELL_W 28
#define CELL_H 26
#define GAP 4
#define MARGIN_X 2
#define MARGIN_Y 2
#define CURSOR_W 4
#define CURSOR_H 4
#define COLOR_BACKGROUND 0
#define COLOR_UNLIT 1
#define COLOR_LIT 2
#define COLOR_CURSOR 3
#define CELL_NONE ((int16_t)-1)
// Typed-text line under the grid (the grid ends at y = 178 with the
// 26-pixel cells above).
#define TEXT_STRIP_Y 180
#define TEXT_X 4
#define TEXT_Y 182
#define TEXT_INPUT_X 68
#define MAX_LINE 40
// Verification scanline: sentinel nibbles, then a received-character
// count byte, then every received character as (high nibble, low
// nibble) pixel pairs. An emulator harness reads this row back from
// the framebuffer to prove each typed byte arrived.
#define VERIFY_Y 198
#define VERIFY_SENTINEL 0xA5A5
#define VERIFY_COUNT_X 4
#define VERIFY_CHARS_X 8
#define MAX_RECEIVED 150
// Every field on both verification rows is stamped high nibble first,
// one nibble per pixel: a byte is two pixels wide, a 16-bit value four.
#define VERIFY_BYTE_NIBBLES 2
#define VERIFY_WORD_NIBBLES 4
// Mouse verification scanline (the row below the typed-character one):
// its own 5A5A sentinel, then present/left/right as one nibble each and a
// zero nibble, then jlMouseX and jlMouseY as four nibbles each.
// Re-stamped only when the reported state changes.
#define VERIFY_MOUSE_Y 199
#define VERIFY_MOUSE_SENTINEL 0x5A5A
#define VERIFY_MOUSE_FLAGS_X 4
#define VERIFY_MOUSE_XPOS_X 8
#define VERIFY_MOUSE_YPOS_X 12
// The flags field packed as one nibble per predicate, in the order the
// row stamps them. This packing is the ONLY definition of that layout:
// it is both what gets drawn and what the change check compares.
#define MOUSE_FLAG_PRESENT 0x1000
#define MOUSE_FLAG_LEFT 0x0100
#define MOUSE_FLAG_RIGHT 0x0010
static void buildPalette(jlSurfaceT *screen);
static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow);
static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow);
static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit);
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y);
static void drawMouseVerifyRow(jlSurfaceT *screen);
static void drawVerifyRow(jlSurfaceT *screen);
static void initialPaint(jlSurfaceT *screen);
static void logTypedHistory(void);
static uint16_t mouseFlags(void);
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void processTypedChars(jlSurfaceT *screen);
static void redrawTextStrip(jlSurfaceT *screen);
static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint16_t value, int16_t nibbles);
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void updateMouseVerifyRow(jlSurfaceT *screen);
// Keys laid out row-by-row. JL_KEY_NONE cells stay blank. Shape roughly
// resembles a real keyboard (top number row, then QWERTY rows, then a
// cluster of modifiers / arrows / function keys).
static const jlKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = {
{ JL_KEY_1, JL_KEY_2, JL_KEY_3, JL_KEY_4, JL_KEY_5, JL_KEY_6, JL_KEY_7, JL_KEY_8, JL_KEY_9, JL_KEY_0 },
{ JL_KEY_Q, JL_KEY_W, JL_KEY_E, JL_KEY_R, JL_KEY_T, JL_KEY_Y, JL_KEY_U, JL_KEY_I, JL_KEY_O, JL_KEY_P },
{ JL_KEY_A, JL_KEY_S, JL_KEY_D, JL_KEY_F, JL_KEY_G, JL_KEY_H, JL_KEY_J, JL_KEY_K, JL_KEY_L, JL_KEY_BACKSPACE },
{ JL_KEY_Z, JL_KEY_X, JL_KEY_C, JL_KEY_V, JL_KEY_B, JL_KEY_N, JL_KEY_M, JL_KEY_LSHIFT, JL_KEY_RSHIFT, JL_KEY_TAB },
{ JL_KEY_SPACE, JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_LCTRL, JL_KEY_LALT, JL_KEY_UP, JL_KEY_DOWN, JL_KEY_LEFT, JL_KEY_RIGHT, JL_KEY_NONE },
{ JL_KEY_F1, JL_KEY_F2, JL_KEY_F3, JL_KEY_F4, JL_KEY_F5, JL_KEY_F6, JL_KEY_F7, JL_KEY_F8, JL_KEY_F9, JL_KEY_F10 }
};
static bool gCellLit[GRID_ROWS][GRID_COLS];
static int16_t gLastCursorX = -100;
static int16_t gLastCursorY = -100;
static int16_t gLastCursorCol = CELL_NONE;
static int16_t gLastCursorRow = CELL_NONE;
// Last state stamped into the mouse verification row; updateMouseVerifyRow
// only redraws when this tuple changes, so idle frames stay draw-free.
static int16_t gLastMouseRowX = -1;
static int16_t gLastMouseRowY = -1;
static uint16_t gLastMouseRowFlags = 0xFFFF;
// Typed-text line state plus the full received-character history for
// the verification row.
static char gLine[MAX_LINE + 1];
static int16_t gLineLen = 0;
static uint8_t gReceived[MAX_RECEIVED];
static int16_t gReceivedCount = 0;
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i;
// Every entry gets a DISTINCT color (gray ramp for the unused
// ones, red for 15 since 0x0FFF is taken by the cursor) so a
// screenshot-based harness can decode the verification row's
// nibble pixels back to values on ports without direct
// framebuffer reads.
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = (uint16_t)((i << 8) | (i << 4) | i);
}
colors[COLOR_BACKGROUND] = 0x0000; // black
colors[COLOR_UNLIT] = 0x0333; // dark gray
colors[COLOR_LIT] = 0x00F0; // bright green
colors[COLOR_CURSOR] = 0x0FFF; // white
colors[15] = 0x0F00; // red (0x0FFF is the cursor's)
jlPaletteSet(screen, 0, colors);
}
static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow) {
int16_t col;
int16_t row;
int16_t cx;
int16_t cy;
*outCol = CELL_NONE;
*outRow = CELL_NONE;
for (row = 0; row < GRID_ROWS; row++) {
for (col = 0; col < GRID_COLS; col++) {
cx = (int16_t)(MARGIN_X + col * (CELL_W + GAP));
cy = (int16_t)(MARGIN_Y + row * (CELL_H + GAP));
if (px >= cx && px < (cx + CELL_W) && py >= cy && py < (cy + CELL_H)) {
*outCol = col;
*outRow = row;
return;
}
}
}
}
static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow) {
jlKeyE key;
key = gKeyGrid[row][col];
if (key == JL_KEY_NONE) {
return false;
}
if (jlKeyDown(key)) {
return true;
}
if (col == cursorCol && row == cursorRow && jlMouseDown(JL_MOUSE_BUTTON_LEFT)) {
return true;
}
return false;
}
static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit) {
int16_t x;
int16_t y;
uint8_t color;
x = (int16_t)(MARGIN_X + col * (CELL_W + GAP));
y = (int16_t)(MARGIN_Y + row * (CELL_H + GAP));
color = lit ? COLOR_LIT : COLOR_UNLIT;
jlFillRect(screen, x, y, CELL_W, CELL_H, color);
}
// The block is clipped against the verification scanlines rather than
// drawn over them: emulator harnesses sample those rows between
// frames, so even a stamp-then-repair within one loop iteration is a
// visible corruption window to them.
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y) {
int16_t h;
h = CURSOR_H;
if ((int16_t)(y + h) > VERIFY_Y) {
h = (int16_t)(VERIFY_Y - y);
}
if (h <= 0) {
return;
}
jlFillRect(screen, x, y, CURSOR_W, h, COLOR_CURSOR);
}
// Stamp the mouse verification scanline: sentinel, present/button
// flags, then the pointer position as nibble pixels. An emulator
// harness reads this row back from the framebuffer to prove mouse
// motion and button state crossed the HAL (see verify-x68000-mouse.sh).
static void drawMouseVerifyRow(jlSurfaceT *screen) {
stampNibbles(screen, 0, VERIFY_MOUSE_Y, VERIFY_MOUSE_SENTINEL, VERIFY_WORD_NIBBLES);
stampNibbles(screen, VERIFY_MOUSE_FLAGS_X, VERIFY_MOUSE_Y, mouseFlags(), VERIFY_WORD_NIBBLES);
stampNibbles(screen, VERIFY_MOUSE_XPOS_X, VERIFY_MOUSE_Y, (uint16_t)jlMouseX(), VERIFY_WORD_NIBBLES);
stampNibbles(screen, VERIFY_MOUSE_YPOS_X, VERIFY_MOUSE_Y, (uint16_t)jlMouseY(), VERIFY_WORD_NIBBLES);
}
// Re-stamp the verification scanline: sentinel, received count, then
// every received character as two nibble pixels. Only the count and
// the newest characters actually change, but the row is cheap enough
// to redraw whole.
static void drawVerifyRow(jlSurfaceT *screen) {
int16_t i;
stampNibbles(screen, 0, VERIFY_Y, VERIFY_SENTINEL, VERIFY_WORD_NIBBLES);
stampNibbles(screen, VERIFY_COUNT_X, VERIFY_Y, (uint16_t)gReceivedCount, VERIFY_BYTE_NIBBLES);
for (i = 0; i < gReceivedCount; i++) {
stampNibbles(screen, (int16_t)(VERIFY_CHARS_X + VERIFY_BYTE_NIBBLES * i), VERIFY_Y, gReceived[i], VERIFY_BYTE_NIBBLES);
}
// The strip repaint that called us blanked the mouse row too, so
// restore it in the same pass.
drawMouseVerifyRow(screen);
}
static void initialPaint(jlSurfaceT *screen) {
int16_t col;
int16_t row;
jlKeyE key;
jlSurfaceClear(screen, COLOR_BACKGROUND);
for (row = 0; row < GRID_ROWS; row++) {
for (col = 0; col < GRID_COLS; col++) {
key = gKeyGrid[row][col];
if (key == JL_KEY_NONE) {
continue;
}
drawCell(screen, col, row, false);
gCellLit[row][col] = false;
}
}
redrawTextStrip(screen);
jlStagePresent();
}
// Emit the received-character history as hex on exit so an emulator
// harness can verify the typed-character path from the boot volume
// when it cannot read the framebuffer (see verify-amiga-input.sh).
static void logTypedHistory(void) {
static const char kHex[] = "0123456789ABCDEF";
char line[2 * MAX_RECEIVED + 8];
int16_t i;
int16_t n;
n = 0;
for (i = 0; i < gReceivedCount; i++) {
line[n] = kHex[(gReceived[i] >> 4) & 0x0F];
line[n + 1] = kHex[gReceived[i] & 0x0F];
n = (int16_t)(n + 2);
}
line[n] = '\0';
jlLogF("typed=%s", line);
jlLogFlush();
}
// The mouse predicates packed one nibble per flag, in the order
// drawMouseVerifyRow stamps them. Sampled once per use so the drawn
// row and updateMouseVerifyRow's change check can never disagree.
static uint16_t mouseFlags(void) {
return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0) | (jlMouseDown(JL_MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0) | (jlMouseDown(JL_MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0));
}
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
int16_t col;
int16_t row;
jlKeyE key;
bool lit;
for (row = 0; row < GRID_ROWS; row++) {
for (col = 0; col < GRID_COLS; col++) {
key = gKeyGrid[row][col];
if (key == JL_KEY_NONE) {
continue;
}
lit = cellTargetLit(col, row, cursorCol, cursorRow);
if (lit == gCellLit[row][col]) {
continue;
}
// drawCell marks the cell's rect dirty; jlStagePresent
// flushes that one band.
drawCell(screen, col, row, lit);
jlStagePresent();
gCellLit[row][col] = lit;
}
}
}
// Pop everything the user typed since last frame, apply it to the
// text line (Backspace deletes, Return clears, Escape is the quit key
// and types nothing), and refresh the strip when anything changed.
static void processTypedChars(jlSurfaceT *screen) {
int ch;
bool changed;
changed = false;
while ((ch = jlInputGetChar()) != -1) {
if (gReceivedCount < MAX_RECEIVED) {
gReceived[gReceivedCount] = (uint8_t)ch;
gReceivedCount = (int16_t)(gReceivedCount + 1);
}
changed = true;
if (ch == JL_CHAR_BACKSPACE) {
if (gLineLen > 0) {
gLineLen = (int16_t)(gLineLen - 1);
}
} else if (ch == JL_CHAR_RETURN) {
gLineLen = 0;
} else if (ch == JL_CHAR_ESCAPE || ch == JL_CHAR_TAB) {
// Nothing to render for these.
} else if (gLineLen < MAX_LINE) {
gLine[gLineLen] = (char)ch;
gLineLen = (int16_t)(gLineLen + 1);
}
}
if (!changed) {
return;
}
redrawTextStrip(screen);
jlStagePresent();
}
// Repaint everything below the key grid: the prompt, the current text
// line, and the verification scanline. Shared by the typed-character
// refresh and the cursor-erase path (which may stamp background over
// this region).
static void redrawTextStrip(jlSurfaceT *screen) {
gLine[gLineLen] = '\0';
jlFillRect(screen, 0, TEXT_STRIP_Y, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT - TEXT_STRIP_Y, COLOR_BACKGROUND);
exampleFontDraw(screen, TEXT_X, TEXT_Y, "TYPE HERE:", COLOR_UNLIT);
exampleFontDraw(screen, TEXT_INPUT_X, TEXT_Y, gLine, COLOR_CURSOR);
drawVerifyRow(screen);
}
// Stamp a value into consecutive pixels as nibbles, high nibble
// first -- the encoding both verification rows and every emulator
// harness that decodes them share.
static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint16_t value, int16_t nibbles) {
int16_t i;
int16_t shift;
for (i = 0; i < nibbles; i++) {
shift = (int16_t)(4 * (nibbles - 1 - i));
jlDrawPixel(screen, (int16_t)(x + i), y, (uint8_t)((value >> shift) & 0x0F));
}
}
// Erase the previous cursor (by redrawing the cell that held it) and
// stamp the new cursor at the current mouse position. Both rects are
// presented; if the cursor stayed inside the same cell only one rect
// pair is touched, so steady-state cost is small.
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
int16_t mouseX;
int16_t mouseY;
mouseX = jlMouseX();
mouseY = jlMouseY();
if (gLastCursorX != mouseX || gLastCursorY != mouseY) {
if (gLastCursorCol != CELL_NONE) {
drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]);
} else if (gLastCursorX >= 0 && gLastCursorY >= 0) {
// Old cursor was in a gap region. Stamp background over it,
// then repair the text strip / verify row if the stamp
// reached into them.
jlFillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND);
if ((int16_t)(gLastCursorY + CURSOR_H) > TEXT_STRIP_Y) {
redrawTextStrip(screen);
}
}
}
drawCursor(screen, mouseX, mouseY);
// All draw calls above marked their rects dirty; one jlStagePresent
// flushes the union (cursor erase + cursor draw).
jlStagePresent();
gLastCursorX = mouseX;
gLastCursorY = mouseY;
gLastCursorCol = cursorCol;
gLastCursorRow = cursorRow;
}
// Re-stamp the mouse verification row only when the reported state
// changed since the last stamp, so idle frames stay draw-free.
static void updateMouseVerifyRow(jlSurfaceT *screen) {
int16_t x;
int16_t y;
uint16_t flags;
x = jlMouseX();
y = jlMouseY();
flags = mouseFlags();
if (x == gLastMouseRowX && y == gLastMouseRowY && flags == gLastMouseRowFlags) {
return;
}
drawMouseVerifyRow(screen);
jlStagePresent();
gLastMouseRowX = x;
gLastMouseRowY = y;
gLastMouseRowFlags = flags;
}
int main(void) {
jlConfigT config;
jlSurfaceT *screen;
int16_t cursorCol;
int16_t cursorRow;
config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
screen = jlStageGet();
if (screen == NULL) {
fprintf(stderr, "jlStageGet returned NULL\n");
jlShutdown();
return 1;
}
buildPalette(screen);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
initialPaint(screen);
jlInputPoll();
for (;;) {
jlInputPoll();
if (jlKeyPressed(JL_KEY_ESCAPE)) {
break;
}
cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow);
presentChangedCells(screen, cursorCol, cursorRow);
updateCursor(screen, cursorCol, cursorRow);
updateMouseVerifyRow(screen);
processTypedChars(screen);
}
logTypedHistory();
jlShutdown();
return 0;
}