diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 07a3242..27945c5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -23,6 +23,13 @@ All notable changes to the HAS (High Assembler) project will be documented in th - positive examples must compile - negative examples must fail - non-zero exit code on any mismatch (CI-friendly) +- **GUI EditBox control** in `lib/gui.s` / `lib/gui.i`: + - Added `DrawEditBox(x,y,w,h,bg,border,text_ptr,tc,cursor_pos,cursor_vis)` single-line editable text field renderer. + - Added `EditBoxProcessKey(text_ptr,max_len,cursor_pos_ptr,scancode)` for scan-code-driven insert/delete/cursor movement. + - Added `EditBoxPollKey(text_ptr,max_len,cursor_pos_ptr)` convenience wrapper consuming `keyboard.s` `current_key`. + - Added `GADGET_TYPE_EDITBOX=2` and `EDITBOX_*` struct layout (28 bytes, `0..19` layout-compatible with `GADGET`). + - `DrawGadget(gadget_ptr)` now dispatches edit boxes (type 2), including focused border and cursor visibility via `EDITBOX_FLAGS`. + - Added end-to-end example `examples/editbox_demo.has`. ### Changed diff --git a/docs/GUI_LIBRARY.md b/docs/GUI_LIBRARY.md index 23b5128..25a69bb 100644 --- a/docs/GUI_LIBRARY.md +++ b/docs/GUI_LIBRARY.md @@ -15,6 +15,9 @@ Reads `gfx_current_mode` at call time — no recompilation needed when switching - [DrawBox](#drawbox) - [DrawMsgBox](#drawmsgbox) - [DrawButton](#drawbutton) ← 3D gadget style + - [DrawEditBox](#draweditbox) + - [EditBoxProcessKey](#editboxprocesskey) + - [EditBoxPollKey](#editboxpollkey) - [DrawWrappedText](#drawwrappedtext) - [DrawGadget](#drawgadget) ← struct-based dispatch 4. [Mouse event manager](#mouse-event-manager) @@ -22,7 +25,7 @@ Reads `gfx_current_mode` at call time — no recompilation needed when switching - [GuiHitTestRect](#guihittestRect) - [GuiHitTest](#guihittest) ← struct-based - [GetGuiMouseX / GetGuiMouseY](#getguimousex--getguimousey) -5. [GADGET struct](#gadget-struct) +5. [GADGET and EDITBOX structs](#gadget-and-editbox-structs) 6. [Button centering rules](#button-centering-rules) 7. [Sprite cursor integration](#sprite-cursor-integration) 8. [Build integration](#build-integration) @@ -42,6 +45,9 @@ include "gui.i" ```has extern func DrawButton(x:int, y:int, w:int, h:int, bg:int, border:int, str:int, tc:int) -> int; extern func DrawMsgBox(x:int, y:int, w:int, h:int, bg:int, border:int, str:int, tc:int) -> int; +extern func DrawEditBox(x:int, y:int, w:int, h:int, bg:int, border:int, text_ptr:int, tc:int, cursor_pos:int, cursor_vis:int) -> int; +extern func EditBoxProcessKey(text_ptr:int, max_len:int, cursor_pos_ptr:int, scancode:int) -> int; +extern func EditBoxPollKey(text_ptr:int, max_len:int, cursor_pos_ptr:int) -> int; extern func GuiPollMouse() -> void; extern func GuiHitTestRect(x:int, y:int, w:int, h:int) -> int; extern func GetGuiMouseX() -> int; @@ -135,6 +141,84 @@ The `border` argument is therefore the *highlight* colour (typically white/brigh | `0` | black `$000` | Bottom/right shadow (automatic) | | `tc` | white `$FFF` | Label text | +### `DrawEditBox` + +```c +DrawEditBox(x, y, w, h, bg, border, text_ptr, tc, cursor_pos, cursor_vis) -> int +``` + +Draws a single-line text input field with optional caret. + +Rendering order: + +1. Draws background + 1-pixel border (`DrawBox`). +2. Draws visible text with 8-pixel left/right padding. +3. If `cursor_vis != 0`, draws a 1-pixel vertical caret at `cursor_pos`. + +Behavior notes: + +- Text is left-aligned and vertically centered. +- Horizontal scrolling is automatic when text grows beyond visible width. + - `visible_cols = (w - 16) / 8` + - `scroll_start = max(0, cursor_pos - visible_cols + 1)` +- `text_ptr` must point to a writable NUL-terminated byte buffer. + +| Argument | Meaning | +|----------|---------| +| `x, y` | Top-left corner in pixels | +| `w, h` | Dimensions in pixels | +| `bg` | Interior background palette index | +| `border` | Border palette index | +| `text_ptr` | Pointer to NUL-terminated text buffer | +| `tc` | Text + caret colour palette index | +| `cursor_pos` | Cursor character index (`0..strlen`) | +| `cursor_vis` | `0` hide caret, non-zero show caret | + +### `EditBoxProcessKey` + +```c +EditBoxProcessKey(text_ptr, max_len, cursor_pos_ptr, scancode) -> int +``` + +Processes one raw keyboard scan code and updates the edit buffer/cursor. + +- Ignores key-release events (`bit 7` set in scan code). +- Maintains NUL-terminated buffer integrity. +- Supports printable insertion, backspace delete, left/right cursor movement. + +Handled control keys: + +- `$41` Backspace +- `$44` Return +- `$43` Keypad Enter +- `$45` Escape +- `$4F` Left arrow +- `$4E` Right arrow + +Return codes: + +| Return | Meaning | +|--------|---------| +| `0` | No change / key-up / unhandled key | +| `1` | Buffer or cursor changed | +| `2` | Return (submit) | +| `3` | Escape (cancel) | + +### `EditBoxPollKey` + +```c +EditBoxPollKey(text_ptr, max_len, cursor_pos_ptr) -> int +``` + +Convenience wrapper around `EditBoxProcessKey`: + +- Reads `current_key` from `lib/keyboard.s` +- Clears it (consumes event) +- Calls `EditBoxProcessKey` +- Returns the same `0/1/2/3` status code + +Use this when the edit box is the sole keyboard consumer in the frame loop. + ### `DrawWrappedText` ```c @@ -162,9 +246,10 @@ Struct-based dispatch: reads `GADGET_TYPE` from the struct and calls the appropr |---------------|-------|-------------| | `GADGET_TYPE_MSGBOX` | 0 | `DrawMsgBox` | | `GADGET_TYPE_BUTTON` | 1 | `DrawButton` | +| `GADGET_TYPE_EDITBOX` | 2 | `DrawEditBox` | Unknown types are silently skipped (returns 0). -See [GADGET struct](#gadget-struct) below for the field layout. +See [GADGET and EDITBOX structs](#gadget-and-editbox-structs) below for field layouts. --- @@ -232,7 +317,7 @@ No stack frame or arguments — equivalent to the `GetMouseX/Y` pattern in `lib/ --- -## GADGET struct +## GADGET and EDITBOX structs Defined in `lib/gui.i`. Size = **20 bytes**. @@ -246,7 +331,7 @@ Defined in `lib/gui.i`. Size = **20 bytes**. | `GADGET_BORDER` | 10 | word | Border / highlight palette index | | `GADGET_TEXT` | 12 | long | Pointer to null-terminated label/message string | | `GADGET_TCOLOR` | 16 | word | Text colour palette index | -| `GADGET_TYPE` | 18 | word | Gadget type selector (`GADGET_TYPE_MSGBOX=0`, `GADGET_TYPE_BUTTON=1`) | +| `GADGET_TYPE` | 18 | word | Gadget type selector (`GADGET_TYPE_MSGBOX=0`, `GADGET_TYPE_BUTTON=1`, `GADGET_TYPE_EDITBOX=2`) | Assembly allocation example: @@ -265,6 +350,31 @@ my_button: dc.w GADGET_TYPE_BUTTON ``` +### EDITBOX struct + +Defined in `lib/gui.i`. Size = **28 bytes**. Offsets `0..19` are layout-compatible with `GADGET`. + +| Field | Offset | Type | Meaning | +|-------|--------|------|---------| +| `EDITBOX_X` | 0 | word | Screen X position (pixels) | +| `EDITBOX_Y` | 2 | word | Screen Y position (pixels) | +| `EDITBOX_W` | 4 | word | Width (pixels) | +| `EDITBOX_H` | 6 | word | Height (pixels) | +| `EDITBOX_BG` | 8 | word | Interior fill palette index | +| `EDITBOX_BORDER` | 10 | word | Inactive border palette index | +| `EDITBOX_TEXTBUF` | 12 | long | Pointer to mutable text buffer | +| `EDITBOX_TCOLOR` | 16 | word | Text/caret colour | +| `EDITBOX_TYPE` | 18 | word | Must be `GADGET_TYPE_EDITBOX` | +| `EDITBOX_MAXLEN` | 20 | word | Maximum characters (excluding NUL) | +| `EDITBOX_CURSOR` | 22 | word | Current cursor position | +| `EDITBOX_FLAGS` | 24 | word | bit0=focused, bit1=cursor visible | +| `EDITBOX_ABORDER` | 26 | word | Active border colour | + +When passed to `DrawGadget`: + +- Border colour uses `EDITBOX_ABORDER` when `EDITBOX_FLAGS bit0 == 1`. +- Caret visibility uses `EDITBOX_FLAGS bit1`. + --- ## Button centering rules @@ -346,7 +456,10 @@ Pass `&cursor` (the address of the height word) to `CreateSprite`. Link `lib/gui.s`, `lib/sprite.s` (if using cursor), `lib/input.s`, `lib/graphics.s`, `lib/font8x8.s`, `lib/helpers.s`, and `lib/takeover.s` together: ```bash -# Using the provided build script: +# Using the generic build script: +./scripts/build_example.sh examples/editbox_demo.has + +# Or existing dedicated demo script: ./scripts/build_msgbox_demo.sh # Manual steps: @@ -359,4 +472,4 @@ vlink -bamigahunk build/msgbox_demo.o build/gui.o build/graphics.o \ build/input.o -o build/msgbox_demo.exe ``` -See [examples/msgbox_demo.has](../examples/msgbox_demo.has) for a complete working demo. +See [examples/msgbox_demo.has](../examples/msgbox_demo.has) and [examples/editbox_demo.has](../examples/editbox_demo.has) for complete working demos. diff --git a/examples/editbox_demo.has b/examples/editbox_demo.has new file mode 100644 index 0000000..bc953c3 --- /dev/null +++ b/examples/editbox_demo.has @@ -0,0 +1,193 @@ +// editbox_demo.has +// Demonstrates the GUI edit box widget: keyboard text entry, cursor navigation, +// submit on Return, cancel (clear) on Escape, exit via button click. +// +// Graphics mode: 320x256x32 (mode 0, 5 bitplanes, line-interleaved) +// Font: lib/font8x8.s (8x8 pixel characters) +// +// Palette indices: +// 0 = black ($000) — screen background +// 1 = white ($FFF) — text, inactive border +// 3 = cyan ($0FF) — active/focused border highlight +// 4 = blue ($00F) — edit box interior +// 8 = grey ($444) — button/label background +// +// Controls: +// Type characters — enter text (lowercase letters, digits, punctuation) +// Left / Right — move cursor +// Backspace — delete char left of cursor +// Return — submit: show entered text in a confirmation box +// Escape — clear input buffer +// Click Exit btn — quit +// +// Build: +// python3 -m hasc.cli examples/editbox_demo.has -o build/editbox_demo.s +// vasmm68k_mot -Fhunkexe -I lib/ -o build/editbox_demo.o \ +// build/editbox_demo.s lib/gui.s lib/graphics.s lib/font8x8.s \ +// lib/helpers.s lib/input.s lib/keyboard.s lib/takeover.s +// vlink -bamigahunk build/editbox_demo.o -o build/editbox_demo.exe + +#pragma lockreg(a5); + +extern var fonts: int; + +code demo: + + extern func SetGraphicsMode(mode: int) -> int; + extern func ClearScreen() -> int; + extern func SetFont(ptr: int) -> int; + extern func SetColor(id: int, col: int) -> int; + extern func UpdateCopperList() -> int; + extern func WaitVBlank() -> void; + extern func TakeSystem() -> void; + extern func ReleaseSystem() -> void; + + // GUI drawing + extern func DrawButton(x:int, y:int, w:int, h:int, bg:int, border:int, + str:int, tc:int) -> int; + extern func DrawMsgBox(x:int, y:int, w:int, h:int, bg:int, border:int, + str:int, tc:int) -> int; + extern func DrawEditBox(x:int, y:int, w:int, h:int, bg:int, border:int, + text_ptr:int, tc:int, + cursor_pos:int, cursor_vis:int) -> int; + + // Keyboard and edit-box input + extern func InitKeyboard() -> void; + extern func EditBoxPollKey(text_ptr:int, max_len:int, + cursor_pos_ptr:int) -> int; + + // Mouse (for Exit button hit-test) + extern func ReadMouse() -> void; + extern func GuiPollMouse() -> void; + extern func GuiHitTestRect(x:int, y:int, w:int, h:int) -> int; + extern func GetGuiMouseX() -> int; + extern func GetGuiMouseY() -> int; + // Sprite cursor + extern func CreateSprite(idx:int, ptr:int) -> int; + extern func ApplySpritePalette(idx:int) -> int; + extern func ShowSprite(idx:int) -> int; + extern func SetSpritePosition(idx:int, x:int, y:int) -> int; + + public main; + + asm { + jsr TakeSystem + jsr main + jmp ReleaseSystem + } + + proc main() -> int { + var result: int; + var changed: int; + var done: int = 0; + var mx: int; + var my: int; + + // --- Graphics setup --- + result = SetGraphicsMode(0); + if (result == -1) { return -1; } + call SetFont(&fonts); + call SetColor(0, $000); // black + call SetColor(1, $FFF); // white + call SetColor(3, $0FF); // cyan (active border) + call SetColor(4, $00F); // blue (edit box bg) + call SetColor(8, $444); // grey (button bg) + call ClearScreen(); + + // --- Keyboard --- + call InitKeyboard(); + + // --- Sprite cursor --- + result = CreateSprite(0, &cursor); + call ApplySpritePalette(0); + call ShowSprite(0); + call SetSpritePosition(0, 160, 128); + + // --- Draw static frame --- + call DrawStaticFrame(); + + // Initial edit box draw (steady visible cursor). + result = DrawEditBox(8, 48, 256, 24, 4, 3, &edit_buf, 1, + edit_cursor, 1); + + call UpdateCopperList(); + + // --- Main loop --- + while (done == 0) { + + call WaitVBlank(); + call ReadMouse(); + call GuiPollMouse(); + + // Move hardware sprite cursor to GUI mouse position. + mx = GetGuiMouseX(); + my = GetGuiMouseY(); + call SetSpritePosition(0, mx, my); + + // Process keyboard + changed = EditBoxPollKey(&edit_buf, 31, &edit_cursor); + + if (changed == 3) { + // Escape: clear buffer, reset cursor, force redraw + edit_buf[0] = 0; + edit_cursor = 0; + changed = 1; + } + + if (changed == 2) { + // Return / Enter: redraw the whole screen showing submitted text + call ClearScreen(); + call DrawStaticFrame(); + result = DrawMsgBox(8, 142, 304, 48, 8, 1, &edit_buf, 1); + } + + // Redraw edit box only when buffer/cursor changed. + if (changed != 0) { + result = DrawEditBox(8, 48, 256, 24, 4, 3, &edit_buf, 1, + edit_cursor, 1); + } + + // Exit button click + if (GuiHitTestRect(104, 224, 112, 24) == 1) { + done = 1; + } + + call UpdateCopperList(); + } + + return 0; + } + + // Draw the static parts of the UI (title, hint, exit button). + // Called on init and after Return (redraws background). + proc DrawStaticFrame() -> void { + var result: int; + result = DrawMsgBox(8, 8, 304, 16, 8, 1, &lbl_title, 1); + result = DrawMsgBox(8, 28, 304, 16, 8, 1, &lbl_hint, 1); + result = DrawButton(104, 224, 112, 24, 8, 1, &lbl_exit, 1); + } + +// ------------------------------------------------------------------------- +// Initialized data (strings, in DATA hunk) +// ------------------------------------------------------------------------- + +data demo_data: + lbl_title.b = "Edit Box Demo", 0 + lbl_hint.b = "Type, arrow keys, backspace, Enter=confirm, Esc=clear.", 0 + lbl_exit.b = "Exit", 0 + + // Sprite cursor data: 4 palette words immediately before sprite body. + cursor_pal.w = $000, $FFF, $CCC, $888 + cursor.w = 11, 0, 0, + $8000, 0, $C000, 0, $E000, 0, $F000, 0, $F800, 0, + $FC00, 0, $FE00, 0, $EC00, 0, $C600, 0, $8300, 0, $0300, 0, + 0, 0 + +// ------------------------------------------------------------------------- +// Uninitialized data (text buffer + cursor, zeroed by loader) +// ------------------------------------------------------------------------- + +bss demo_bss: + edit_buf.b: 32 // 32-byte text buffer (31 usable chars + NUL) + edit_cursor.w: 1 // current cursor position (0..strlen) + diff --git a/lib/gui.i b/lib/gui.i index 40e031f..d2ae7ff 100644 --- a/lib/gui.i +++ b/lib/gui.i @@ -38,6 +38,30 @@ GADGET_SIZE EQU 20 ; struct size in bytes (for allocation) GADGET_TYPE_MSGBOX EQU 0 ; message box with word-wrapped text GADGET_TYPE_BUTTON EQU 1 ; clickable button with centred label +GADGET_TYPE_EDITBOX EQU 2 ; keyboard-driven text input field + +; ============================================================ +; EDITBOX struct layout (28 bytes) — offsets 0–19 are identical +; to the GADGET struct so DrawGadget dispatch works transparently. +; Allocate EDITBOX_SIZE bytes; pass pointer to DrawGadget or the +; flat DrawEditBox / EditBoxPollKey functions. +; ============================================================ + +EDITBOX_X EQU 0 ; word - screen X position in pixels +EDITBOX_Y EQU 2 ; word - screen Y position in pixels +EDITBOX_W EQU 4 ; word - width in pixels (multiples of 8 recommended) +EDITBOX_H EQU 6 ; word - height in pixels (16 or 24 recommended) +EDITBOX_BG EQU 8 ; word - interior fill palette index +EDITBOX_BORDER EQU 10 ; word - inactive border colour palette index +EDITBOX_TEXTBUF EQU 12 ; long - pointer to buffer of at least EDITBOX_MAXLEN+1 bytes +EDITBOX_TCOLOR EQU 16 ; word - text colour palette index +EDITBOX_TYPE EQU 18 ; word = GADGET_TYPE_EDITBOX (2) +; --- EditBox-specific fields (offsets 20–27) --- +EDITBOX_MAXLEN EQU 20 ; word - max storable characters (not counting NUL) +EDITBOX_CURSOR EQU 22 ; word - current cursor position (0..strlen) +EDITBOX_FLAGS EQU 24 ; word - bit 0 = focused, bit 1 = cursor visible +EDITBOX_ABORDER EQU 26 ; word - focused/active border colour palette index +EDITBOX_SIZE EQU 28 ; total struct size in bytes ; ============================================================ ; External references (assembled in gui.s) @@ -51,6 +75,9 @@ GADGET_TYPE_BUTTON EQU 1 ; clickable button with centred label XREF DrawMsgBox XREF DrawButton XREF DrawGadget + XREF DrawEditBox + XREF EditBoxProcessKey + XREF EditBoxPollKey XREF GuiPollMouse XREF GuiHitTest XREF GuiHitTestRect @@ -71,6 +98,9 @@ GADGET_TYPE_BUTTON EQU 1 ; clickable button with centred label ; extern func DrawMsgBox(x:int, y:int, w:int, h:int, bg:int, border:int, str:int, tc:int) -> int; ; extern func DrawButton(x:int, y:int, w:int, h:int, bg:int, border:int, str:int, tc:int) -> int; ; extern func DrawGadget(gadget_ptr:int) -> int; +; extern func DrawEditBox(x:int, y:int, w:int, h:int, bg:int, border:int, text_ptr:int, tc:int, cursor_pos:int, cursor_vis:int) -> int; +; extern func EditBoxProcessKey(text_ptr:int, max_len:int, cursor_pos_ptr:int, scancode:int) -> int; +; extern func EditBoxPollKey(text_ptr:int, max_len:int, cursor_pos_ptr:int) -> int; ; extern func GuiPollMouse() -> void; ; extern func GuiHitTest(gadget_ptr:int) -> int; ; extern func GuiHitTestRect(x:int, y:int, w:int, h:int) -> int; diff --git a/lib/gui.s b/lib/gui.s index 8ace336..a9680f4 100644 --- a/lib/gui.s +++ b/lib/gui.s @@ -43,6 +43,9 @@ XDEF DrawMsgBox XDEF DrawButton XDEF DrawGadget + XDEF DrawEditBox + XDEF EditBoxProcessKey + XDEF EditBoxPollKey XDEF GuiPollMouse XDEF GuiHitTest XDEF GuiHitTestRect @@ -57,6 +60,7 @@ XREF GetMouseDX XREF GetMouseDY XREF GetMouseLBtn + XREF current_key ; ============================================================ @@ -602,13 +606,84 @@ DrawGadget: move.l 8(a6),a0 ; a0 = gadget struct pointer - ; Dispatch on GADGET_TYPE: 0=msgbox, 1=button; skip unknown + ; Dispatch on GADGET_TYPE: 0=msgbox, 1=button, 2=editbox; skip unknown move.w 18(a0),d1 ; 18 = GADGET_TYPE offset tst.w d1 beq .dg_dispatch ; type 0: msgbox cmp.w #1,d1 + beq .dg_dispatch ; type 1: button + cmp.w #2,d1 bne .dg_done ; unknown type: skip +; --- EditBox dispatch (GADGET_TYPE_EDITBOX = 2) --- +; DrawEditBox(x, y, w, h, bg, border, text_ptr, tc, cursor_pos, cursor_vis) +; Border: use EDITBOX_ABORDER (offset 26) when bit 0 of EDITBOX_FLAGS (offset 24) is set, +; otherwise use EDITBOX_BORDER (offset 10). +; cursor_vis: bit 1 of EDITBOX_FLAGS. +.dg_editbox: + ; cursor_vis = (EDITBOX_FLAGS >> 1) & 1 → arg10 + moveq #0,d1 + move.w 24(a0),d1 ; EDITBOX_FLAGS + lsr.w #1,d1 + and.w #1,d1 + move.l d1,-(sp) ; arg10 = cursor_vis + + ; cursor_pos = EDITBOX_CURSOR → arg9 + moveq #0,d1 + move.w 22(a0),d1 ; EDITBOX_CURSOR + move.l d1,-(sp) ; arg9 = cursor_pos + + ; tc = EDITBOX_TCOLOR → arg8 + moveq #0,d1 + move.w 16(a0),d1 ; EDITBOX_TCOLOR + move.l d1,-(sp) ; arg8 = tc + + ; text_ptr = EDITBOX_TEXTBUF → arg7 + move.l 12(a0),-(sp) ; arg7 = text_ptr (long) + + ; border: focused → EDITBOX_ABORDER; inactive → EDITBOX_BORDER → arg6 + moveq #0,d1 + move.w 24(a0),d1 ; EDITBOX_FLAGS + btst #0,d1 ; focused? + bne .dg_eb_use_active + moveq #0,d1 + move.w 10(a0),d1 ; EDITBOX_BORDER + bra .dg_eb_push_border +.dg_eb_use_active: + moveq #0,d1 + move.w 26(a0),d1 ; EDITBOX_ABORDER +.dg_eb_push_border: + move.l d1,-(sp) ; arg6 = border + + ; bg = EDITBOX_BG → arg5 + moveq #0,d1 + move.w 8(a0),d1 ; EDITBOX_BG + move.l d1,-(sp) ; arg5 = bg + + ; h = EDITBOX_H → arg4 + moveq #0,d1 + move.w 6(a0),d1 ; EDITBOX_H + move.l d1,-(sp) ; arg4 = h + + ; w = EDITBOX_W → arg3 + moveq #0,d1 + move.w 4(a0),d1 ; EDITBOX_W + move.l d1,-(sp) ; arg3 = w + + ; y = EDITBOX_Y (signed) → arg2 + move.w 2(a0),d1 + ext.l d1 + move.l d1,-(sp) ; arg2 = y + + ; x = EDITBOX_X (signed) → arg1 + move.w 0(a0),d1 + ext.l d1 + move.l d1,-(sp) ; arg1 = x + + jsr DrawEditBox + lea 40(sp),sp + bra .dg_done + .dg_dispatch: ; Push all 8 args (identical layout for DrawMsgBox and DrawButton) moveq #0,d1 @@ -782,6 +857,382 @@ DrawButton: rts +; ============================================================ +; DrawEditBox(x, y, w, h, bg, border, text_ptr, tc, cursor_pos, cursor_vis) -> int +; +; 8(a6) = x left pixel (long) +; 12(a6) = y top pixel (long) +; 16(a6) = w width in pixels (long) +; 20(a6) = h height in pixels (long) +; 24(a6) = bg interior fill palette index (long) +; 28(a6) = border 1-px frame palette index (long) +; 32(a6) = text_ptr pointer to NUL-terminated text buffer (long) +; 36(a6) = tc text colour palette index (long) +; 40(a6) = cursor_pos character index where caret is drawn (long) +; 44(a6) = cursor_vis 0 = hide caret, non-zero = draw caret (long) +; +; Renders the edit box: +; 1. Fills interior + 1-px border via DrawBox. +; 2. Draws text left-aligned with 8-px left padding, vertically centred. +; Scrolls text so cursor_pos is always visible (stateless; cursor +; anchors to the rightmost visible column when string is long). +; 3. If cursor_vis != 0, draws a 1-px vertical caret bar at cursor_pos. +; +; visible_cols = (w - 16) / 8 (8-px padding each side) +; scroll_start = max(0, cursor_pos - visible_cols + 1) +; Returns d0 = 0. +; ============================================================ +DrawEditBox: + link a6,#-8 ; -4(a6)=start_char -8(a6)=visible_cols + movem.l d1-d7/a0-a2,-(sp) + + ; --- Compute visible_cols = (w - 16) / 8 --- + move.l 16(a6),d0 ; w + sub.l #16,d0 + ble .deb_exit ; w too small: nothing to draw + lsr.l #3,d0 ; / 8 + move.l d0,-8(a6) ; visible_cols + + ; --- Compute scroll start (stateless) --- + ; if cursor_pos < visible_cols: start = 0 + ; else: start = cursor_pos - visible_cols + 1 + move.l 40(a6),d1 ; cursor_pos + cmp.l -8(a6),d1 + blt .deb_start_zero + move.l d1,d0 + sub.l -8(a6),d0 + addq.l #1,d0 + move.l d0,-4(a6) ; start_char + bra .deb_start_done +.deb_start_zero: + clr.l -4(a6) +.deb_start_done: + + ; --- Draw border + fill via DrawBox --- + move.l 28(a6),-(sp) ; border + move.l 24(a6),-(sp) ; bg + move.l 20(a6),-(sp) ; h + move.l 16(a6),-(sp) ; w + move.l 12(a6),-(sp) ; y + move.l 8(a6),-(sp) ; x + jsr DrawBox + lea 24(sp),sp + + ; --- Compute text cursor starting position --- + ; cx_start = (x + 8) >> 3 (char column, snapped to 8-px grid) + move.l 8(a6),d0 + add.l #8,d0 + lsr.l #3,d0 + move.w d0,d7 ; d7 = cx_start (saved) + move.w d0,gfx_text_cursor_x + + ; cy = (y + h/2) >> 3 (vertically centred char row) + move.l 20(a6),d0 ; h + lsr.l #1,d0 ; h / 2 + add.l 12(a6),d0 ; y + h/2 + lsr.l #3,d0 ; >> 3 + move.w d0,d6 ; d6 = cy (saved) + move.w d0,gfx_text_cursor_y + + move.l 36(a6),d5 ; d5 = tc (text colour, constant) + + ; --- Skip start_char bytes in text_ptr --- + move.l 32(a6),a0 ; a0 = text_ptr + move.l -4(a6),d2 ; d2 = start_char + beq .deb_skip_done +.deb_skip_loop: + tst.b (a0) ; reached NUL before start_char? + beq .deb_skip_done + addq.l #1,a0 + subq.l #1,d2 + bne .deb_skip_loop +.deb_skip_done: + + ; --- Draw up to visible_cols chars --- + move.l -8(a6),d4 ; d4 = remaining columns +.deb_draw_loop: + tst.l d4 + beq .deb_draw_done + moveq #0,d0 + move.b (a0)+,d0 + beq .deb_draw_done ; NUL = end of string + move.l d5,d1 ; d1 = tc + jsr _DrawChar + move.w gfx_text_cursor_x,d1 + addq.w #1,d1 + move.w d1,gfx_text_cursor_x + subq.l #1,d4 + bra .deb_draw_loop +.deb_draw_done: + + ; --- Draw 1-px caret if cursor_vis != 0 --- + tst.l 44(a6) + beq .deb_exit + + ; cursor_col = cursor_pos - start_char (0-based from text area left) + move.l 40(a6),d0 ; cursor_pos + sub.l -4(a6),d0 ; - start_char + tst.l d0 + blt .deb_exit + cmp.l -8(a6),d0 ; > visible_cols? + bgt .deb_exit + + ; cursor_px_x = x + 8 + cursor_col * 8 + lsl.l #3,d0 ; cursor_col * 8 + add.l 8(a6),d0 ; + x + add.l #8,d0 ; + left padding + + ; FillRect(cursor_px_x, y+2, 1, h-4, tc) + move.l 36(a6),-(sp) ; color = tc + move.l 20(a6),d1 + sub.l #4,d1 + move.l d1,-(sp) ; h - 4 + move.l #1,-(sp) ; w = 1 + move.l 12(a6),d1 + add.l #2,d1 + move.l d1,-(sp) ; y + 2 + move.l d0,-(sp) ; cursor_px_x + jsr FillRect + lea 20(sp),sp + +.deb_exit: + moveq #0,d0 + movem.l (sp)+,d1-d7/a0-a2 + unlk a6 + rts + + +; ============================================================ +; EditBoxProcessKey(text_ptr, max_len, cursor_pos_ptr, scancode) -> int +; +; 8(a6) = text_ptr pointer to NUL-terminated buffer (long) +; 12(a6) = max_len buffer capacity, not counting NUL (long) +; 16(a6) = cursor_pos_ptr pointer to a word holding cursor position (long) +; 20(a6) = scancode raw value from GetKey() (long) +; +; Bit 7 of scancode signals a key-release event → returns 0 (no change). +; All edits maintain a valid NUL-terminated string in the buffer. +; +; Returns: +; 0 key-up event, or unhandled key (no change) +; 1 buffer or cursor changed (caller should redraw) +; 2 Return / KP-Enter pressed (caller: treat as submit) +; 3 Escape pressed (caller: treat as cancel) +; +; Handled keys: +; Printable chars ($00-$7F lookup) — insert at cursor, advance cursor +; $41 Backspace — delete char left of cursor, retreat cursor +; $44 Return — return 2 +; $43 KP Enter — return 2 +; $45 Escape — return 3 +; $4F Left arrow — move cursor left +; $4E Right arrow— move cursor right +; +; Registers saved: d1-d5 / a0-a1 +; ============================================================ +EditBoxProcessKey: + link a6,#-4 ; -4(a6) = strlen (temp) + movem.l d1-d5/a0-a1,-(sp) + + ; --- Key-up check: bit 7 set → release event, ignore --- + move.l 20(a6),d0 ; scancode + btst #7,d0 + bne .ebpk_nochange + + and.l #$7F,d0 ; strip release bit → base scan code + move.l d0,d5 ; d5 = scan code (constant) + + ; Load cursor position + move.l 16(a6),a1 ; a1 = cursor_pos_ptr + moveq #0,d3 + move.w (a1),d3 ; d3 = cursor_pos (zero-extended word) + + ; --- Escape ($45) --- + cmp.l #$45,d5 + bne .ebpk_not_esc + moveq #3,d0 + bra .ebpk_ret +.ebpk_not_esc: + + ; --- Return ($44) and KP Enter ($43) --- + cmp.l #$44,d5 + beq .ebpk_return + cmp.l #$43,d5 + bne .ebpk_not_return +.ebpk_return: + moveq #2,d0 + bra .ebpk_ret +.ebpk_not_return: + + ; --- Backspace ($41): delete char left of cursor --- + cmp.l #$41,d5 + bne .ebpk_not_bs + tst.l d3 + beq .ebpk_nochange ; cursor at start: nothing to delete + + ; Measure strlen + move.l 8(a6),a0 + clr.l -4(a6) +.ebpk_bs_len: + tst.b (a0)+ + beq .ebpk_bs_len_done + addq.l #1,-4(a6) + bra .ebpk_bs_len +.ebpk_bs_len_done: + + ; Decrement cursor (d3 = new cursor_pos = old - 1) + subq.l #1,d3 + + ; Shift text[old_cursor..strlen] one byte left (forward copy, no overlap) + ; dst = text_ptr + d3, src = text_ptr + d3 + 1 + ; count = strlen - d3 bytes (chars from old_cursor to NUL inclusive) + move.l 8(a6),a0 + add.l d3,a0 ; a0 = dst + move.l a0,a1 + addq.l #1,a1 ; a1 = src + move.l -4(a6),d4 + sub.l d3,d4 ; d4 = strlen - new_cursor_pos + subq.l #1,d4 ; d4 = count - 1 (for dbra) +.ebpk_bs_copy: + move.b (a1)+,(a0)+ + dbra d4,.ebpk_bs_copy + + move.l 16(a6),a1 ; reload cursor_pos_ptr (a1 was used by copy loop) + move.w d3,(a1) + moveq #1,d0 + bra .ebpk_ret +.ebpk_not_bs: + + ; --- Left arrow ($4F): move cursor left --- + cmp.l #$4F,d5 + bne .ebpk_not_left + tst.l d3 + beq .ebpk_nochange + subq.l #1,d3 + move.w d3,(a1) + moveq #1,d0 + bra .ebpk_ret +.ebpk_not_left: + + ; --- Right arrow ($4E): move cursor right --- + cmp.l #$4E,d5 + bne .ebpk_not_right + ; Measure strlen to clamp cursor + move.l 8(a6),a0 + clr.l -4(a6) +.ebpk_rt_len: + tst.b (a0)+ + beq .ebpk_rt_len_done + addq.l #1,-4(a6) + bra .ebpk_rt_len +.ebpk_rt_len_done: + cmp.l -4(a6),d3 + bge .ebpk_nochange ; cursor already at end + addq.l #1,d3 + move.w d3,(a1) + moveq #1,d0 + bra .ebpk_ret +.ebpk_not_right: + + ; --- Printable character: table lookup --- + cmp.l #$7F,d5 + bgt .ebpk_nochange ; out of table range + lea editbox_sc_unshifted,a0 + moveq #0,d0 + move.b (a0,d5.w),d0 ; d0 = ASCII char (0 = not mapped) + beq .ebpk_nochange + + ; Measure strlen to check against max_len + move.l 8(a6),a0 + clr.l -4(a6) +.ebpk_ins_len: + tst.b (a0)+ + beq .ebpk_ins_len_done + addq.l #1,-4(a6) + bra .ebpk_ins_len +.ebpk_ins_len_done: + move.l -4(a6),d1 ; d1 = strlen + cmp.l 12(a6),d1 ; strlen >= max_len? + bge .ebpk_nochange ; buffer full + + ; Shift text[cursor_pos..strlen] one byte right (backwards to avoid overlap) + ; src start = text_ptr + strlen (NUL), dst = text_ptr + strlen + 1 + ; count = strlen - cursor_pos + 1 (including NUL) + ; dbra counter = count - 1 = strlen - cursor_pos + move.l 8(a6),a0 + add.l -4(a6),a0 ; a0 = text_ptr + strlen (src: NUL byte) + move.l a0,a1 + addq.l #1,a1 ; a1 = dst (one past NUL) + move.l -4(a6),d4 + sub.l d3,d4 ; d4 = strlen - cursor_pos (dbra counter) +.ebpk_ins_shift: + move.b (a0),(a1) ; backwards copy + subq.l #1,a0 + subq.l #1,a1 + dbra d4,.ebpk_ins_shift + + ; Write new char at cursor_pos + move.l 8(a6),a0 + add.l d3,a0 ; a0 = text_ptr + cursor_pos + move.b d0,(a0) ; store char + + ; Advance cursor + addq.l #1,d3 + move.l 16(a6),a1 ; reload cursor_pos_ptr (a1 may be stale) + move.w d3,(a1) + moveq #1,d0 + bra .ebpk_ret + +.ebpk_nochange: + moveq #0,d0 +.ebpk_ret: + movem.l (sp)+,d1-d5/a0-a1 + unlk a6 + rts + + +; ============================================================ +; EditBoxPollKey(text_ptr, max_len, cursor_pos_ptr) -> int +; +; 8(a6) = text_ptr (same as EditBoxProcessKey) +; 12(a6) = max_len +; 16(a6) = cursor_pos_ptr +; +; Reads `current_key` from keyboard.s, clears it (consumes the event), +; and calls EditBoxProcessKey with the raw scan code. +; Returns the same values as EditBoxProcessKey (0/1/2/3). +; If current_key is 0 (no pending key), returns 0 immediately. +; +; Note: do not call GetKey() separately in the same frame when using +; this function — they both consume current_key. +; ============================================================ +EditBoxPollKey: + link a6,#0 + movem.l d1/a0,-(sp) + + moveq #0,d1 + move.b current_key,d1 ; read pending scan code + beq .ebpl_nochange ; 0 = no key pressed + + clr.b current_key ; consume the event + + ; Forward to EditBoxProcessKey + move.l d1,-(sp) ; arg4 = scancode + move.l 16(a6),-(sp) ; arg3 = cursor_pos_ptr + move.l 12(a6),-(sp) ; arg2 = max_len + move.l 8(a6),-(sp) ; arg1 = text_ptr + jsr EditBoxProcessKey + lea 16(sp),sp + bra .ebpl_done + +.ebpl_nochange: + moveq #0,d0 +.ebpl_done: + movem.l (sp)+,d1/a0 + unlk a6 + rts + + ; ============================================================ ; GuiPollMouse() ; Must be called once per frame AFTER ReadMouse(). @@ -1002,3 +1453,33 @@ gui_abs_mouse_x: dc.w 0 ; accumulated absolute X pixel (clamped to screen widt gui_abs_mouse_y: dc.w 0 ; accumulated absolute Y pixel (clamped to 0..255) gui_lbtn_prev: dc.w 0 ; left-button state from previous GuiPollMouse call gui_lbtn_edge: dc.w 0 ; 1 on the frame the left button is first pressed, else 0 + +; ============================================================ +; editbox_sc_unshifted — Amiga scan-code → ASCII table (128 entries) +; +; Index = scan code after keyboard.s ror/not processing (bit 7 stripped). +; 0x00 means "not a printable character" (special keys handled separately). +; Covers the unshifted US/international layout. Keypad digits are mapped +; to their ASCII digit equivalents so numeric entry works without shift. +; ============================================================ +editbox_sc_unshifted: + ; $00-$07 ` 1 2 3 4 5 6 7 + dc.b $60,$31,$32,$33,$34,$35,$36,$37 + ; $08-$0F 8 9 0 - = \(intl) [unused] 0(KP0) + dc.b $38,$39,$30,$2D,$3D,$5C,$00,$30 + ; $10-$17 q w e r t y u i + dc.b $71,$77,$65,$72,$74,$79,$75,$69 + ; $18-$1F o p [ ] [unused] 1(KP1) 2(KP2) 3(KP3) + dc.b $6F,$70,$5B,$5D,$00,$31,$32,$33 + ; $20-$27 a s d f g h j k + dc.b $61,$73,$64,$66,$67,$68,$6A,$6B + ; $28-$2F l ; ' \ [unused] 4(KP4) 5(KP5) 6(KP6) + dc.b $6C,$3B,$27,$5C,$00,$34,$35,$36 + ; $30-$37 <(intl) z x c v b n m + dc.b $3C,$7A,$78,$63,$76,$62,$6E,$6D + ; $38-$3F , . / .(KP.) [unused] 7(KP7) 8(KP8) 9(KP9) + dc.b $2C,$2E,$2F,$2E,$00,$37,$38,$39 + ; $40-$47 space [BS=special] [Tab=0] [KPEnter=special] [Return=special] [ESC=special] [Del] [Help] + dc.b $20,$00,$00,$00,$00,$00,$00,$00 + ; $48-$7F arrows, function keys, modifiers — all non-printable + dcb.b 56,$00 diff --git a/lib/keyboardcodes.i b/lib/keyboardcodes.i index 97e9d0e..77e8384 100644 --- a/lib/keyboardcodes.i +++ b/lib/keyboardcodes.i @@ -1,106 +1,110 @@ ;**************************************************************** -; Generic Amiga keyboard scan-code mapping (starter file) +; Generic Amiga keyboard scan-code mapping ; -; ESC is known to be $45 in this project. -; This file provides symbolic `KEY_*` equates for common keys so RAL -; and assembly code can include a single header for key constants. +; Scan codes are the values stored in `current_key` by the CIA +; interrupt handler in keyboard.s (after ror #1 / not.b processing). +; Bit 7 is SET on key-release events; mask with $7F for the base code. ; -; NOTE: Only `KEY_ESC` is set to the value used in the repo. Other -; scancodes are initialised to $00 as placeholders — replace them -; with hardware-verified scan codes for your keyboard/layout when -; you have them. +; Arrow keys and ESC/Space are used by existing game examples. +; All other values are verified against the Amiga HRM scan-code table. ;**************************************************************** IFND KEYBOARD_CODES_I KEYBOARD_CODES_I SET 1 ; --- Special keys ----------------------------------------------- -KEY_ESC equ $45 ; Escape (verified in project) -KEY_TAB equ $00 ; Tab (placeholder) -KEY_SPACE equ 64 ; Space -KEY_RETURN equ $00 ; Enter / Return -KEY_BACKSP equ $00 ; Backspace / Delete +KEY_ESC equ $45 ; Escape +KEY_TAB equ $42 ; Tab +KEY_SPACE equ $40 ; Space +KEY_RETURN equ $44 ; Enter / Return +KEY_BACKSP equ $41 ; Backspace / Delete +KEY_DEL equ $46 ; Delete (forward) +KEY_HELP equ $47 ; Help ; --- Modifiers ------------------------------------------------- -KEY_LSHIFT equ $00 -KEY_RSHIFT equ $00 -KEY_LCTRL equ $00 -KEY_RCTRL equ $00 -KEY_LALT equ $00 -KEY_RALT equ $00 -KEY_CAPS equ $00 +KEY_LSHIFT equ $60 +KEY_RSHIFT equ $61 +KEY_CAPS equ $62 +KEY_LCTRL equ $63 +KEY_LALT equ $64 +KEY_RALT equ $65 +KEY_LAMIGA equ $66 +KEY_RAMIGA equ $67 +; Note: right Ctrl shares $63 on many Amiga models ; --- Arrow keys ------------------------------------------------ -KEY_LEFT equ 79 -KEY_RIGHT equ 78 -KEY_UP equ 76 -KEY_DOWN equ 77 +KEY_UP equ $4C ; decimal 76 +KEY_DOWN equ $4D ; decimal 77 +KEY_RIGHT equ $4E ; decimal 78 +KEY_LEFT equ $4F ; decimal 79 + +; --- Keypad Enter ---------------------------------------------- +KEY_KPENTER equ $43 ; --- Function keys --------------------------------------------- -KEY_F1 equ $00 -KEY_F2 equ $00 -KEY_F3 equ $00 -KEY_F4 equ $00 -KEY_F5 equ $00 -KEY_F6 equ $00 -KEY_F7 equ $00 -KEY_F8 equ $00 -KEY_F9 equ $00 -KEY_F10 equ $00 -KEY_F11 equ $00 -KEY_F12 equ $00 +KEY_F1 equ $50 +KEY_F2 equ $51 +KEY_F3 equ $52 +KEY_F4 equ $53 +KEY_F5 equ $54 +KEY_F6 equ $55 +KEY_F7 equ $56 +KEY_F8 equ $57 +KEY_F9 equ $58 +KEY_F10 equ $59 -; --- Numeric digits ------------------------------------------- -KEY_0 equ $00 -KEY_1 equ $00 -KEY_2 equ $00 -KEY_3 equ $00 -KEY_4 equ $00 -KEY_5 equ $00 -KEY_6 equ $00 -KEY_7 equ $00 -KEY_8 equ $00 -KEY_9 equ $00 +; --- Numeric digits (main keyboard row) ----------------------- +KEY_1 equ $01 +KEY_2 equ $02 +KEY_3 equ $03 +KEY_4 equ $04 +KEY_5 equ $05 +KEY_6 equ $06 +KEY_7 equ $07 +KEY_8 equ $08 +KEY_9 equ $09 +KEY_0 equ $0A -; --- Letters A..Z --------------------------------------------- -KEY_A equ $00 -KEY_B equ $00 -KEY_C equ $00 -KEY_D equ $00 -KEY_E equ $00 -KEY_F equ $00 -KEY_G equ $00 -KEY_H equ $00 -KEY_I equ $00 -KEY_J equ $00 -KEY_K equ $00 -KEY_L equ $00 -KEY_M equ $00 -KEY_N equ $00 -KEY_O equ $00 -KEY_P equ $00 -KEY_Q equ $00 -KEY_R equ $00 -KEY_S equ $00 -KEY_T equ $00 -KEY_U equ $00 -KEY_V equ $00 -KEY_W equ $00 -KEY_X equ $00 -KEY_Y equ $00 -KEY_Z equ $00 +; --- Letters A..Z (by scan-code order, not alphabetical) ------ +KEY_Q equ $10 +KEY_W equ $11 +KEY_E equ $12 +KEY_R equ $13 +KEY_T equ $14 +KEY_Y equ $15 +KEY_U equ $16 +KEY_I equ $17 +KEY_O equ $18 +KEY_P equ $19 +KEY_A equ $20 +KEY_S equ $21 +KEY_D equ $22 +KEY_F equ $23 +KEY_G equ $24 +KEY_H equ $25 +KEY_J equ $26 +KEY_K equ $27 +KEY_L equ $28 +KEY_Z equ $31 +KEY_X equ $32 +KEY_C equ $33 +KEY_V equ $34 +KEY_B equ $35 +KEY_N equ $36 +KEY_M equ $37 ; --- Punctuation / other common keys --------------------------- -KEY_MINUS equ $00 ; - _ -KEY_EQUALS equ $00 ; = + -KEY_LBRACE equ $00 ; [ { -KEY_RBRACE equ $00 ; ] } -KEY_SEMI equ $00 ; ; : -KEY_QUOTE equ $00 ; ' " -KEY_COMMA equ $00 ; , < -KEY_DOT equ $00 ; . > -KEY_SLASH equ $00 ; / ? -KEY_BSLASH equ $00 ; \ | KEY_GRAVE equ $00 ; ` ~ +KEY_MINUS equ $0B ; - _ +KEY_EQUALS equ $0C ; = + +KEY_BSLASH equ $0D ; \ | (international / European row) +KEY_LBRACE equ $1A ; [ { +KEY_RBRACE equ $1B ; ] } +KEY_SEMI equ $29 ; ; : +KEY_QUOTE equ $2A ; ' " +KEY_INTL equ $30 ; < > (European extra key between LShift and Z) +KEY_COMMA equ $38 ; , < +KEY_DOT equ $39 ; . > +KEY_SLASH equ $3A ; / ? ENDC diff --git a/scripts/build.sh b/scripts/build.sh index 41d4257..6c3d310 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,17 +1,93 @@ #!/usr/bin/env bash set -euo pipefail -# Build script: assemble generated out.s with vasm and link with vlink -# Ensure vasm and vlink are in PATH +# Generic single-file build helper. +# Supports input as: +# - .s : assemble + link +# - .has : compile (hasc) + assemble + link +# +# Usage: +# ./scripts/build.sh [out.o] [out.exe] -OUT_S=${1:-out.s} -OUT_O=${2:-out.o} -OUT_EXE=${3:-out.exe} +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +INPUT=${1:-out.s} + +if [[ "$INPUT" = /* ]]; then + INPUT_ABS="$INPUT" +else + INPUT_ABS="$(cd "$(dirname "$INPUT")" && pwd)/$(basename "$INPUT")" +fi + +if [[ ! -f "$INPUT_ABS" ]]; then + echo "ERROR: input not found: $INPUT" >&2 + exit 1 +fi + +BASE_NAME="$(basename "${INPUT_ABS%.*}")" +OUT_O=${2:-"$BASE_NAME.o"} +OUT_EXE=${3:-"$BASE_NAME.exe"} + +mkdir -p "$(dirname "$OUT_O")" "$(dirname "$OUT_EXE")" + +VASM="${VASM:-vasmm68k_mot}" +if ! command -v "$VASM" &>/dev/null; then + if command -v vasm68000_mot &>/dev/null; then + VASM="vasm68000_mot" + else + BUNDLED="$HOME/.vscode/extensions/prb28.amiga-assembly-1.8.13/resources/bin/linux/vasmm68k_mot" + if [[ -x "$BUNDLED" ]]; then + VASM="$BUNDLED" + else + echo "ERROR: assembler not found (tried: vasmm68k_mot, vasm68000_mot)." >&2 + echo "Set VASM=/path/to/vasmm68k_mot" >&2 + exit 1 + fi + fi +fi + +VLINK="${VLINK:-vlink}" +if ! command -v "$VLINK" &>/dev/null; then + echo "ERROR: vlink not found. Set VLINK=/path/to/vlink" >&2 + exit 1 +fi + +if [[ -x "$ROOT/.venv/bin/python" ]]; then + PYTHON="${HASC_PYTHON:-$ROOT/.venv/bin/python}" +elif [[ -x "$ROOT/venv/bin/python" ]]; then + PYTHON="${HASC_PYTHON:-$ROOT/venv/bin/python}" +elif command -v python3 &>/dev/null; then + PYTHON="${HASC_PYTHON:-python3}" +else + PYTHON="${HASC_PYTHON:-python}" +fi + +TMP_S="" +OUT_S="$INPUT_ABS" +INPUT_EXT="${INPUT_ABS##*.}" + +if [[ "$INPUT_EXT" == "has" ]]; then + TMP_S="${TMPDIR:-/tmp}/${BASE_NAME}_$$.s" + OUT_S="$TMP_S" + echo "Compiling $INPUT -> $OUT_S" + (cd "$ROOT" && "$PYTHON" -m hasc.cli "$INPUT_ABS" -o "$OUT_S") +elif [[ "$INPUT_EXT" != "s" ]]; then + echo "ERROR: unsupported input extension '.$INPUT_EXT' (expected .has or .s)" >&2 + exit 1 +fi + +cleanup() { + if [[ -n "$TMP_S" && -f "$TMP_S" ]]; then + rm -f "$TMP_S" + fi +} +trap cleanup EXIT echo "Assembling $OUT_S -> $OUT_O" -vasm68000_mot -Fhunkexe -o "$OUT_O" "$OUT_S" +"$VASM" -Fhunkexe -o "$OUT_O" "$OUT_S" echo "Linking $OUT_O -> $OUT_EXE" -vlink -bamigahunk "$OUT_O" -o "$OUT_EXE" +"$VLINK" -bamigahunk "$OUT_O" -o "$OUT_EXE" echo "Built $OUT_EXE" diff --git a/scripts/build_example.sh b/scripts/build_example.sh new file mode 100644 index 0000000..873c2a6 --- /dev/null +++ b/scripts/build_example.sh @@ -0,0 +1,249 @@ +#!/usr/bin/env bash +# build_example.sh +# Generic build helper for HAS examples: +# .has -> .s (hasc) -> .o (vasm) -> .exe (vlink) +# +# Usage: +# ./scripts/build_example.sh +# ./scripts/build_example.sh snake.has +# ./scripts/build_example.sh examples/snake.has +# +# Environment overrides: +# HASC_PYTHON=/path/to/python Python used for hasc (default: auto) +# VASM=/path/to/vasmm68k_mot Assembler (default: auto) +# VLINK=/path/to/vlink Linker (default: vlink) +# +# Notes: +# - Library objects are selected automatically from extern symbols in the +# source file and then extended with a small dependency closure. +# - If no external symbol is detected, only the compiled example object is linked. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +BUILD="$ROOT/build" +LIB_DIR="$ROOT/lib" + +usage() { + cat <<'EOF' +Usage: ./scripts/build_example.sh + +Examples: + ./scripts/build_example.sh snake.has + ./scripts/build_example.sh examples/snake.has + ./scripts/build_example.sh examples/games/robots/robots.has +EOF +} + +if [[ $# -ne 1 ]]; then + usage + exit 2 +fi + +INPUT="$1" + +if [[ -f "$ROOT/$INPUT" ]]; then + SRC="$ROOT/$INPUT" +elif [[ -f "$ROOT/examples/$INPUT" ]]; then + SRC="$ROOT/examples/$INPUT" +else + echo "ERROR: .has source not found: $INPUT" >&2 + exit 1 +fi + +case "$SRC" in + *.has) ;; + *) + echo "ERROR: source must be a .has file: $SRC" >&2 + exit 1 + ;; +esac + +if [[ -x "$ROOT/.venv/bin/python" ]]; then + PYTHON="${HASC_PYTHON:-$ROOT/.venv/bin/python}" +elif [[ -x "$ROOT/venv/bin/python" ]]; then + PYTHON="${HASC_PYTHON:-$ROOT/venv/bin/python}" +elif command -v python3 &>/dev/null; then + PYTHON="${HASC_PYTHON:-python3}" +else + PYTHON="${HASC_PYTHON:-python}" +fi + +VASM="${VASM:-vasmm68k_mot}" +if ! command -v "$VASM" &>/dev/null; then + BUNDLED="$HOME/.vscode/extensions/prb28.amiga-assembly-1.8.13/resources/bin/linux/vasmm68k_mot" + if [[ -x "$BUNDLED" ]]; then + VASM="$BUNDLED" + else + echo "ERROR: vasmm68k_mot not found. Set VASM=/path/to/vasmm68k_mot." >&2 + exit 1 + fi +fi + +VLINK="${VLINK:-vlink}" +if ! command -v "$VLINK" &>/dev/null; then + echo "ERROR: vlink not found. Set VLINK=/path/to/vlink." >&2 + exit 1 +fi + +mkdir -p "$BUILD" + +REL_SRC="${SRC#$ROOT/}" +BASE_NAME="$(basename "${SRC%.has}")" +OUT_S="$BUILD/$BASE_NAME.s" +OUT_O="$BUILD/$BASE_NAME.o" +OUT_EXE="$BUILD/$BASE_NAME.exe" + +# Exclude alternate font object to avoid duplicate symbol 'fonts'. +LIB_SOURCES=( + "$LIB_DIR/gui.s" + "$LIB_DIR/graphics.s" + "$LIB_DIR/font8x8.s" + "$LIB_DIR/helpers.s" + "$LIB_DIR/takeover.s" + "$LIB_DIR/input.s" + "$LIB_DIR/keyboard.s" + "$LIB_DIR/sprite.s" + "$LIB_DIR/str.s" + "$LIB_DIR/heap.s" + "$LIB_DIR/math.s" + "$LIB_DIR/bob.s" + "$LIB_DIR/ptplayer.s" +) + +declare -A SYM_TO_LIB=() +for lib in "${LIB_SOURCES[@]}"; do + while IFS= read -r sym; do + [[ -z "$sym" ]] && continue + [[ "$sym" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + # Keep first provider to avoid accidental overrides. + if [[ -z "${SYM_TO_LIB[$sym]:-}" ]]; then + SYM_TO_LIB["$sym"]="$lib" + fi + done < <( + grep -Eio '^[[:space:]]*xdef[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' "$lib" \ + | sed -E 's/^[[:space:]]*[xX][dD][eE][fF][[:space:]]+//' \ + | sort -u + ) +done + +mapfile -t EXTERN_SYMBOLS < <( + grep -Eio '^[[:space:]]*extern[[:space:]]+(func|var)[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' "$SRC" \ + | sed -E 's/^[[:space:]]*[eE][xX][tT][eE][rR][nN][[:space:]]+([fF][uU][nN][cC]|[vV][aA][rR])[[:space:]]+//' \ + | sort -u +) + +# Resolve libraries directly from extern symbols. +declare -A WANT_LIB=() +for sym in "${EXTERN_SYMBOLS[@]}"; do + [[ "$sym" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + lib="${SYM_TO_LIB[$sym]:-}" + if [[ -n "$lib" ]]; then + WANT_LIB["$lib"]=1 + fi +done + +# Lightweight dependency closure for known cross-lib references. +# This keeps linking stable for common GUI/graphics/input stacks. +add_dep() { + local lib="$1" + WANT_LIB["$lib"]=1 +} + +changed=1 +while [[ $changed -eq 1 ]]; do + changed=0 + for lib in "${!WANT_LIB[@]}"; do + case "$(basename "$lib")" in + gui.s) + for dep in "$LIB_DIR/graphics.s" "$LIB_DIR/input.s"; do + if [[ -z "${WANT_LIB[$dep]:-}" ]]; then add_dep "$dep"; changed=1; fi + done + ;; + graphics.s) + for dep in "$LIB_DIR/helpers.s" "$LIB_DIR/sprite.s" "$LIB_DIR/takeover.s"; do + if [[ -z "${WANT_LIB[$dep]:-}" ]]; then add_dep "$dep"; changed=1; fi + done + ;; + input.s) + dep="$LIB_DIR/helpers.s" + if [[ -z "${WANT_LIB[$dep]:-}" ]]; then add_dep "$dep"; changed=1; fi + ;; + heap.s) + dep="$LIB_DIR/helpers.s" + if [[ -z "${WANT_LIB[$dep]:-}" ]]; then add_dep "$dep"; changed=1; fi + ;; + str.s) + dep="$LIB_DIR/helpers.s" + if [[ -z "${WANT_LIB[$dep]:-}" ]]; then add_dep "$dep"; changed=1; fi + ;; + bob.s) + for dep in "$LIB_DIR/graphics.s" "$LIB_DIR/helpers.s"; do + if [[ -z "${WANT_LIB[$dep]:-}" ]]; then add_dep "$dep"; changed=1; fi + done + ;; + ptplayer.s) + dep="$LIB_DIR/takeover.s" + if [[ -z "${WANT_LIB[$dep]:-}" ]]; then add_dep "$dep"; changed=1; fi + ;; + esac + done +done + +# Deterministic library order (only include those requested). +ORDERED_LIBS=( + "$LIB_DIR/helpers.s" + "$LIB_DIR/takeover.s" + "$LIB_DIR/graphics.s" + "$LIB_DIR/font8x8.s" + "$LIB_DIR/input.s" + "$LIB_DIR/keyboard.s" + "$LIB_DIR/sprite.s" + "$LIB_DIR/gui.s" + "$LIB_DIR/str.s" + "$LIB_DIR/heap.s" + "$LIB_DIR/math.s" + "$LIB_DIR/bob.s" + "$LIB_DIR/ptplayer.s" +) + +SELECTED_LIBS=() +for lib in "${ORDERED_LIBS[@]}"; do + if [[ -n "${WANT_LIB[$lib]:-}" ]]; then + SELECTED_LIBS+=("$lib") + fi +done + +echo "=== Build: $REL_SRC ===" +echo " Python: $PYTHON" +echo " VASM : $VASM" +echo " VLINK : $VLINK" + +if [[ ${#SELECTED_LIBS[@]} -eq 0 ]]; then + echo " Libs : (none auto-detected)" +else + echo " Libs :" + for lib in "${SELECTED_LIBS[@]}"; do + echo " - ${lib#$ROOT/}" + done +fi + +echo "[1/3] HAS compile..." +(cd "$ROOT" && "$PYTHON" -m hasc.cli "$REL_SRC" -o "$OUT_S") + +echo "[2/3] Assemble objects..." +VASM_FLAGS=(-Fhunk -devpac -I "$LIB_DIR") +"$VASM" "${VASM_FLAGS[@]}" "$OUT_S" -o "$OUT_O" + +OBJECTS=("$OUT_O") +for lib in "${SELECTED_LIBS[@]}"; do + obj="$BUILD/$(basename "${lib%.s}").o" + "$VASM" "${VASM_FLAGS[@]}" "$lib" -o "$obj" + OBJECTS+=("$obj") +done + +echo "[3/3] Link..." +"$VLINK" -bamigahunk "${OBJECTS[@]}" -o "$OUT_EXE" + +echo "Done: ${OUT_EXE#$ROOT/}"