joeylib2/examples/agi/agiText.c

438 lines
17 KiB
C

// AGI v2 text plane: status line, display() rows, print() modal.
//
// Renders directly to the stage with an embedded 8x8 ASCII bitmap
// font; bypasses jlDrawText so glyph foreground/background can vary
// per-call without rebuilding a font surface for every color combo.
//
// Layout matches AGI's 40x25 text grid:
// Row 0 : status line (when statusLineOn)
// Rows 1..21 : picture (handled by agiPicBlit, not here)
// Rows 22..24 : message overlay area
//
// The print() modal centers a window over the picture region with
// a 1-character border and waits for user ack (HALT_PRINT_PENDING).
#include "agi.h"
#include "joey/draw.h"
#include "joey/surface.h"
#include "surfaceInternal.h"
#include <stddef.h>
#include <string.h>
#ifdef JOEYLIB_PLATFORM_IIGS
segment "AGITXT";
#endif
#define GLYPH_W 8u
#define GLYPH_H 8u
#define FIRST_CHAR 0x20u // space
#define LAST_CHAR 0x7Eu // tilde
#define GLYPH_COUNT (LAST_CHAR - FIRST_CHAR + 1u)
#define STAGE_W SURFACE_WIDTH
#define STAGE_H SURFACE_HEIGHT
// ----- Prototypes -----
static void drawChar(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg, uint8_t bg);
static void drawCharTransparent(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg);
static void drawString(jlSurfaceT *stage, int16_t x, int16_t y, const char *s, uint8_t fg, uint8_t bg);
static void drawWindow(jlSurfaceT *stage, int16_t x, int16_t y, uint8_t cols, uint8_t rows, uint8_t bg, uint8_t border);
static void fillRow(jlSurfaceT *stage, uint8_t row, uint8_t bg);
// 8x8 bitmap font for printable ASCII (0x20 = space .. 0x7E = tilde).
// Each glyph is 8 bytes; bit 7 is the leftmost pixel of each row.
// Lower-case letters map to upper-case via the ASCII normalizer in
// drawChar so the table only needs the upper half.
//
// Hand-pixeled to be readable rather than authentic. Each row is
// commented with the visual pattern (X = on, . = off).
static const uint8_t kFont[GLYPH_COUNT][GLYPH_H] = {
/* 0x20 ' ' */ {0,0,0,0,0,0,0,0},
/* 0x21 '!' */ {0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x00},
/* 0x22 '"' */ {0x6C,0x6C,0x48,0x00,0x00,0x00,0x00,0x00},
/* 0x23 '#' */ {0x36,0x36,0x7F,0x36,0x7F,0x36,0x36,0x00},
/* 0x24 '$' */ {0x18,0x3E,0x60,0x3C,0x06,0x7C,0x18,0x00},
/* 0x25 '%' */ {0x66,0x66,0x0C,0x18,0x30,0x66,0x66,0x00},
/* 0x26 '&' */ {0x38,0x6C,0x38,0x76,0xDC,0xCC,0x76,0x00},
/* 0x27 '\''*/ {0x18,0x18,0x10,0x00,0x00,0x00,0x00,0x00},
/* 0x28 '(' */ {0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00},
/* 0x29 ')' */ {0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00},
/* 0x2A '*' */ {0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00},
/* 0x2B '+' */ {0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00},
/* 0x2C ',' */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30},
/* 0x2D '-' */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
/* 0x2E '.' */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00},
/* 0x2F '/' */ {0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00},
/* 0x30 '0' */ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00},
/* 0x31 '1' */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
/* 0x32 '2' */ {0x3C,0x66,0x06,0x1C,0x30,0x66,0x7E,0x00},
/* 0x33 '3' */ {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
/* 0x34 '4' */ {0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00},
/* 0x35 '5' */ {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
/* 0x36 '6' */ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00},
/* 0x37 '7' */ {0x7E,0x66,0x0C,0x18,0x18,0x18,0x18,0x00},
/* 0x38 '8' */ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00},
/* 0x39 '9' */ {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
/* 0x3A ':' */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x00},
/* 0x3B ';' */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x30},
/* 0x3C '<' */ {0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x00},
/* 0x3D '=' */ {0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00},
/* 0x3E '>' */ {0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x00},
/* 0x3F '?' */ {0x3C,0x66,0x06,0x0C,0x18,0x00,0x18,0x00},
/* 0x40 '@' */ {0x3C,0x66,0x6E,0x6E,0x60,0x62,0x3C,0x00},
/* 0x41 'A' */ {0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00},
/* 0x42 'B' */ {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
/* 0x43 'C' */ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00},
/* 0x44 'D' */ {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
/* 0x45 'E' */ {0x7E,0x60,0x60,0x78,0x60,0x60,0x7E,0x00},
/* 0x46 'F' */ {0x7E,0x60,0x60,0x78,0x60,0x60,0x60,0x00},
/* 0x47 'G' */ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00},
/* 0x48 'H' */ {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
/* 0x49 'I' */ {0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00},
/* 0x4A 'J' */ {0x1E,0x0C,0x0C,0x0C,0x0C,0x6C,0x38,0x00},
/* 0x4B 'K' */ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00},
/* 0x4C 'L' */ {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
/* 0x4D 'M' */ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00},
/* 0x4E 'N' */ {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
/* 0x4F 'O' */ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00},
/* 0x50 'P' */ {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
/* 0x51 'Q' */ {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00},
/* 0x52 'R' */ {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
/* 0x53 'S' */ {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00},
/* 0x54 'T' */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
/* 0x55 'U' */ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00},
/* 0x56 'V' */ {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
/* 0x57 'W' */ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00},
/* 0x58 'X' */ {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
/* 0x59 'Y' */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
/* 0x5A 'Z' */ {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
/* 0x5B '[' */ {0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00},
/* 0x5C '\\'*/ {0xC0,0x60,0x30,0x18,0x0C,0x06,0x02,0x00},
/* 0x5D ']' */ {0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00},
/* 0x5E '^' */ {0x18,0x3C,0x66,0x00,0x00,0x00,0x00,0x00},
/* 0x5F '_' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF},
/* 0x60 '`' */ {0x30,0x18,0x0C,0x00,0x00,0x00,0x00,0x00},
/* 0x61 'a' */ {0x00,0x00,0x3C,0x06,0x3E,0x66,0x3E,0x00},
/* 0x62 'b' */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00},
/* 0x63 'c' */ {0x00,0x00,0x3C,0x66,0x60,0x66,0x3C,0x00},
/* 0x64 'd' */ {0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00},
/* 0x65 'e' */ {0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00},
/* 0x66 'f' */ {0x1C,0x36,0x30,0x78,0x30,0x30,0x30,0x00},
/* 0x67 'g' */ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x3C},
/* 0x68 'h' */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00},
/* 0x69 'i' */ {0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00},
/* 0x6A 'j' */ {0x06,0x00,0x06,0x06,0x06,0x06,0x66,0x3C},
/* 0x6B 'k' */ {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00},
/* 0x6C 'l' */ {0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00},
/* 0x6D 'm' */ {0x00,0x00,0x66,0x7F,0x7F,0x6B,0x63,0x00},
/* 0x6E 'n' */ {0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00},
/* 0x6F 'o' */ {0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00},
/* 0x70 'p' */ {0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60},
/* 0x71 'q' */ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06},
/* 0x72 'r' */ {0x00,0x00,0x7C,0x66,0x60,0x60,0x60,0x00},
/* 0x73 's' */ {0x00,0x00,0x3E,0x60,0x3C,0x06,0x7C,0x00},
/* 0x74 't' */ {0x18,0x18,0x7E,0x18,0x18,0x18,0x0E,0x00},
/* 0x75 'u' */ {0x00,0x00,0x66,0x66,0x66,0x66,0x3E,0x00},
/* 0x76 'v' */ {0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00},
/* 0x77 'w' */ {0x00,0x00,0x63,0x6B,0x7F,0x7F,0x36,0x00},
/* 0x78 'x' */ {0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00},
/* 0x79 'y' */ {0x00,0x00,0x66,0x66,0x66,0x3E,0x06,0x3C},
/* 0x7A 'z' */ {0x00,0x00,0x7E,0x0C,0x18,0x30,0x7E,0x00},
/* 0x7B '{' */ {0x0E,0x18,0x18,0x70,0x18,0x18,0x0E,0x00},
/* 0x7C '|' */ {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
/* 0x7D '}' */ {0x70,0x18,0x18,0x0E,0x18,0x18,0x70,0x00},
/* 0x7E '~' */ {0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00}
};
// ----- Internal helpers (alphabetical) -----
static void drawChar(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg, uint8_t bg) {
const uint8_t *glyph;
uint8_t row;
uint8_t col;
uint8_t bits;
uint8_t c;
uint8_t *stagePixels;
c = (uint8_t)ch;
if (c < FIRST_CHAR || c > LAST_CHAR) {
c = FIRST_CHAR; // unknown -> space
}
glyph = kFont[c - FIRST_CHAR];
// Chunky fast path: each glyph row is 8 bits == 4 stage bytes
// (each byte = 2 pixels in 4bpp packed). Build each byte by
// checking 2 bits at a time. Bypasses jlDrawPixel's per-pixel
// overhead which dominates text rendering on slow targets.
stagePixels = (stage != NULL) ? stage->pixels : NULL;
if (stagePixels != NULL && x >= 0 && y >= 0 &&
x + (int16_t)GLYPH_W <= (int16_t)SURFACE_WIDTH &&
y + (int16_t)GLYPH_H <= (int16_t)SURFACE_HEIGHT &&
(x & 1) == 0) {
uint16_t rowOff;
uint16_t byteX;
uint8_t bytePair;
uint8_t left;
uint8_t right;
byteX = (uint16_t)(x >> 1);
for (row = 0u; row < GLYPH_H; row++) {
bits = glyph[row];
rowOff = (uint16_t)((y + row) * SURFACE_BYTES_PER_ROW + byteX);
for (col = 0u; col < GLYPH_W; col += 2u) {
left = (uint8_t)((bits & (uint8_t)(0x80u >> col)) ? fg : bg);
right = (uint8_t)((bits & (uint8_t)(0x80u >> (col + 1u))) ? fg : bg);
bytePair = (uint8_t)((left << 4) | right);
stagePixels[rowOff + (col >> 1)] = bytePair;
}
}
surfaceMarkDirtyRect(stage, x, y, (int16_t)GLYPH_W, (int16_t)GLYPH_H);
return;
}
// Planar / out-of-bounds slow fallback.
for (row = 0u; row < GLYPH_H; row++) {
bits = glyph[row];
for (col = 0u; col < GLYPH_W; col++) {
uint8_t pixel;
pixel = ((bits >> (uint8_t)(7u - col)) & 0x01u) ? fg : bg;
jlDrawPixel(stage, (int16_t)(x + (int16_t)col), (int16_t)(y + (int16_t)row), pixel);
}
}
}
// Draw a char with transparent background: only foreground bits are
// written, the surface pixels underneath show through. Chunky fast
// path reads each stage byte, modifies set nibbles, writes back.
static void drawCharTransparent(jlSurfaceT *stage, int16_t x, int16_t y, char ch, uint8_t fg) {
const uint8_t *glyph;
uint8_t row;
uint8_t col;
uint8_t bits;
uint8_t c;
uint8_t *stagePixels;
c = (uint8_t)ch;
if (c < FIRST_CHAR || c > LAST_CHAR) {
return; // unknown -> nothing to draw (transparent)
}
glyph = kFont[c - FIRST_CHAR];
stagePixels = (stage != NULL) ? stage->pixels : NULL;
if (stagePixels != NULL && x >= 0 && y >= 0 &&
x + (int16_t)GLYPH_W <= (int16_t)SURFACE_WIDTH &&
y + (int16_t)GLYPH_H <= (int16_t)SURFACE_HEIGHT &&
(x & 1) == 0) {
uint16_t rowOff;
uint16_t byteX;
uint8_t existing;
uint8_t leftMask;
uint8_t rightMask;
uint8_t nibbleFg;
byteX = (uint16_t)(x >> 1);
nibbleFg = (uint8_t)(fg & 0x0Fu);
for (row = 0u; row < GLYPH_H; row++) {
bits = glyph[row];
if (bits == 0u) { continue; } // entire row transparent
rowOff = (uint16_t)((y + row) * SURFACE_BYTES_PER_ROW + byteX);
for (col = 0u; col < GLYPH_W; col += 2u) {
leftMask = (uint8_t)(bits & (uint8_t)(0x80u >> col));
rightMask = (uint8_t)(bits & (uint8_t)(0x80u >> (col + 1u)));
if (leftMask == 0u && rightMask == 0u) { continue; }
existing = stagePixels[rowOff + (col >> 1)];
if (leftMask) { existing = (uint8_t)((existing & 0x0Fu) | (nibbleFg << 4)); }
if (rightMask) { existing = (uint8_t)((existing & 0xF0u) | nibbleFg); }
stagePixels[rowOff + (col >> 1)] = existing;
}
}
surfaceMarkDirtyRect(stage, x, y, (int16_t)GLYPH_W, (int16_t)GLYPH_H);
return;
}
// Slow fallback (planar or out-of-bounds).
for (row = 0u; row < GLYPH_H; row++) {
bits = glyph[row];
for (col = 0u; col < GLYPH_W; col++) {
if (bits & (uint8_t)(0x80u >> col)) {
jlDrawPixel(stage, (int16_t)(x + (int16_t)col), (int16_t)(y + (int16_t)row), fg);
}
}
}
}
static void drawString(jlSurfaceT *stage, int16_t x, int16_t y, const char *s, uint8_t fg, uint8_t bg) {
int16_t cur;
if (s == NULL) {
return;
}
cur = x;
while (*s != '\0') {
if (cur >= (int16_t)STAGE_W) {
return;
}
drawChar(stage, cur, y, *s, fg, bg);
cur = (int16_t)(cur + (int16_t)GLYPH_W);
s++;
}
}
static void drawWindow(jlSurfaceT *stage, int16_t x, int16_t y, uint8_t cols, uint8_t rows, uint8_t bg, uint8_t border) {
int16_t w;
int16_t h;
w = (int16_t)((int16_t)cols * (int16_t)GLYPH_W);
h = (int16_t)((int16_t)rows * (int16_t)GLYPH_H);
jlFillRect(stage, x, y, (uint16_t)(w + 4), (uint16_t)(h + 4), border);
jlFillRect(stage, (int16_t)(x + 2), (int16_t)(y + 2), (uint16_t)w, (uint16_t)h, bg);
}
static void fillRow(jlSurfaceT *stage, uint8_t row, uint8_t bg) {
jlFillRect(stage, 0, (int16_t)((int16_t)row * (int16_t)GLYPH_H), (uint16_t)STAGE_W, (uint16_t)GLYPH_H, bg);
}
// ----- Public API (alphabetical) -----
const uint16_t *agiTextAsciiMap(void) {
// Not used in this impl (we render direct), but expose so callers
// who want to use jlDrawText against agiTextFontSurface have the
// standard 16x6 layout map. Built lazily on first call.
static uint16_t map[256];
static bool built;
uint16_t i;
if (built) {
return map;
}
for (i = 0u; i < 256u; i++) {
if (i < FIRST_CHAR || i > LAST_CHAR) {
map[i] = (uint16_t)0xFFFFu;
} else {
uint8_t off;
off = (uint8_t)(i - FIRST_CHAR);
map[i] = (uint16_t)(((uint16_t)(off / 16u) << 8) | (uint16_t)(off % 16u));
}
}
built = true;
return map;
}
const jlSurfaceT *agiTextFontSurface(void) {
// No surface built in this impl (drawChar walks raw bits direct
// to the stage). Reserved for callers that prefer jlDrawText.
return NULL;
}
void agiTextRender(const AgiVmT *vm, jlSurfaceT *stage) {
uint8_t r;
if (vm->statusLineOn) {
fillRow(stage, vm->statusLineRow, 15u); // white background
if (vm->textRows[vm->statusLineRow].text[0] != '\0') {
drawString(stage, 0, (int16_t)((int16_t)vm->statusLineRow * (int16_t)GLYPH_H),
vm->textRows[vm->statusLineRow].text, 0u, 15u);
}
}
// display() rows: walk every row that has content. Glyphs draw
// transparently (no bg fill) so any PIC art behind shows through
// where glyph pixels are 0. Filling the whole row's bg would
// obscure picture elements that overlap the text band (e.g.
// KQ3's "King's Quest III" title art).
for (r = 0u; r < AGI_TEXT_ROWS; r++) {
const char *p;
int16_t x;
int16_t y;
uint8_t fg;
if (r == vm->statusLineRow && vm->statusLineOn) {
continue; // status line handled above
}
if (vm->textRows[r].text[0] == '\0') {
continue;
}
y = (int16_t)((int16_t)r * (int16_t)GLYPH_H);
x = 0;
fg = vm->textRows[r].fg;
p = vm->textRows[r].text;
while (*p != '\0' && x < (int16_t)STAGE_W) {
if (*p != ' ') {
drawCharTransparent(stage, x, y, *p, fg);
}
x = (int16_t)(x + (int16_t)GLYPH_W);
p++;
}
}
if (vm->printModal.active) {
const char *p;
int16_t lineW;
int16_t maxW;
int16_t lineCount;
int16_t winX;
int16_t winY;
int16_t lineH;
int16_t textY;
int16_t curX;
// Measure: width = longest line in chars; height = lines.
lineW = 0;
maxW = 1;
lineCount = 1;
p = vm->printModal.message;
while (*p != '\0') {
if (*p == '\n') {
if (lineW > maxW) {
maxW = lineW;
}
lineW = 0;
lineCount++;
} else {
lineW++;
}
p++;
}
if (lineW > maxW) {
maxW = lineW;
}
if (maxW > (int16_t)AGI_TEXT_COLS) {
maxW = (int16_t)AGI_TEXT_COLS;
}
lineH = (int16_t)GLYPH_H;
winX = (int16_t)((STAGE_W - (maxW * (int16_t)GLYPH_W + 4)) / 2);
winY = (int16_t)((STAGE_H - (lineCount * lineH + 4)) / 2);
if (winX < 0) { winX = 0; }
if (winY < 0) { winY = 0; }
drawWindow(stage, winX, winY, (uint8_t)maxW, (uint8_t)lineCount, 15u, 4u);
textY = (int16_t)(winY + 2);
curX = (int16_t)(winX + 2);
p = vm->printModal.message;
while (*p != '\0') {
if (*p == '\n') {
textY = (int16_t)(textY + lineH);
curX = (int16_t)(winX + 2);
} else {
drawChar(stage, curX, textY, *p, 0u, 15u);
curX = (int16_t)(curX + (int16_t)GLYPH_W);
}
p++;
}
}
}