// cursor.c - iigs/cursor.h implementation. Push/pop stack of // CursorRecord COPIES so transient cursor swaps (busy, I-beam, etc.) // can be installed and restored without the caller juggling pointers // into Memory-Manager-relocatable storage. // // Phase 2.5 (2026-06-01) scope: thin wrappers + Wait/IBeam ROM shapes. // Embedded cursor blobs are NOT in scope; callers wanting a custom // cursor should construct their own ORCA-shape Cursor record and pass // it to SetCursor() directly. // // The save stack stores 128-byte COPIES (not pointers) - the largest // standard IIgs cursor is 16x16 (ROM arrow): 4-byte header + 32 bytes // data + 32 bytes mask + 4 bytes hotspot = 72 bytes. 128 is generous. // Copying the whole record is mandatory: toolset-owned cursors live in // MM-relocatable handles and the live pointer can move out from under // us between push and pop if the heap compacts. #include "iigs/cursor.h" #include "iigs/toolbox.h" // Size of one save slot. Covers the full 16x16 ROM-style cursor with // headroom for slightly larger custom records (24x16 etc). Pushes of // cursors larger than this truncate the copy and return success - the // pop will then restore a partial record which still has valid header // + data + mask but a (possibly garbage) hotspot. Document this // limitation in the header if a larger cursor ever ships. #define CURSOR_COPY_BYTES 128 // CursorRecord prefix layout (per ORCA quickdraw.h:112-118): // Word cursorHeight - size in BYTES (not pixels) // Word cursorWidth - enclosing rectangle width in WORDS // Word cursorData[] - cursorHeight/2 words of bitmap // Word cursorMask[] - cursorHeight/2 words of mask // Point cursorHotSpot - 4 bytes (h, v) // So total = 4 (header) + 2*cursorHeight (data+mask) + 4 (hotspot). // We compute the live record size from the header so partial copies // don't drag in trailing slop from neighboring Memory Mgr blocks. #define CURSOR_HEADER_BYTES 4 #define CURSOR_HOTSPOT_BYTES 4 static unsigned char gCursorStack[IIGS_CURSOR_STACK_DEPTH][CURSOR_COPY_BYTES]; static unsigned short gCursorStackBytes[IIGS_CURSOR_STACK_DEPTH]; static unsigned short gCursorStackDepth = 0; // Application-registered "default" cursor. Pop returns to this when // the save stack underflows; that way a mismatched push/pop pair still // lands the user in a known cursor instead of leaking ROM state. static const IigsCursorT *gRegisteredCursor = (const IigsCursorT *)0; // Compute the byte length of a live CursorRecord from its header. // Returns 0 if the pointer is NULL. Clamps to CURSOR_COPY_BYTES so // the memcpy below never overruns the save slot. static unsigned short cursorRecordBytes(const void *p) { if (!p) { return 0; } const unsigned short *w = (const unsigned short *)p; // cursorHeight is in bytes; data + mask occupy 2*cursorHeight. unsigned short height = w[0]; unsigned short total = (unsigned short)(CURSOR_HEADER_BYTES + 2U * height + CURSOR_HOTSPOT_BYTES); if (total > CURSOR_COPY_BYTES) { total = CURSOR_COPY_BYTES; } return total; } // Save the currently-active cursor into the next stack slot. Returns // 0 on success, nonzero on stack overflow or NULL live cursor (which // means InitCursor() never ran - the InitCursor invariant from the // header). static uint16_t pushCurrent(void) { if (gCursorStackDepth >= IIGS_CURSOR_STACK_DEPTH) { return 1; } void *live = GetCursorAdr(); if (!live) { // Cursor Mgr never initialized. Hard-error per the // InitCursor invariant - SetCursor on a NULL save buffer // would walk through 0 in ROM. return 2; } unsigned short n = cursorRecordBytes(live); unsigned char *dst = gCursorStack[gCursorStackDepth]; const unsigned char *src = (const unsigned char *)live; for (unsigned short i = 0; i < n; i++) { dst[i] = src[i]; } gCursorStackBytes[gCursorStackDepth] = n; gCursorStackDepth++; return 0; } uint16_t iigsCursorPushArrow(void) { uint16_t rc = pushCurrent(); if (rc != 0) { return rc; } // InitCursor reinstalls the ROM arrow shape without reallocating // the Cursor Mgr save buffer (idempotent after first call from // startdesk()). Same effect as SetCursor(romArrow) without us // having to fish the arrow's address out of toolset internals. InitCursor(); return 0; } uint16_t iigsCursorPushBusy(void) { uint16_t rc = pushCurrent(); if (rc != 0) { return rc; } // WaitCursor (QDAuxiliary 0x0A12) installs the ROM wristwatch // cursor. Internally calls SetCursor on the ROM busy shape. WaitCursor(); return 0; } uint16_t iigsCursorPop(void) { if (gCursorStackDepth == 0) { // Underflow: try the registered fallback so a stray Pop // doesn't leave us with whatever transient cursor happens to // be live. If the application never called Register either, // hard-error so the caller notices the mismatch. if (gRegisteredCursor) { SetCursor((void *)gRegisteredCursor); return 0; } return 1; } gCursorStackDepth--; // Install our saved COPY directly. SetCursor reads the record by // pointer and copies bytes into the Cursor Mgr's working area; it // does NOT retain our pointer past the call, so it's safe to hand // it a pointer into our gCursorStack[]. SetCursor(gCursorStack[gCursorStackDepth]); return 0; } uint16_t iigsCursorRegister(const IigsCursorT *cursor) { gRegisteredCursor = cursor; if (cursor) { if (!GetCursorAdr()) { // InitCursor invariant: refuse to install before the // Cursor Mgr has been brought up. Keep the pointer // registered for a later (post-InitCursor) Pop fallback. return 2; } SetCursor((void *)cursor); } return 0; }