A lightweight, mode-aware widget toolkit for Amiga Motorola 68000 programs written in HAS or plain assembly.
Supports mode 0 (320×256, 5 bitplanes, 32 colours) and mode 1 (640×256, 4 bitplanes, 16 colours).
Reads gfx_current_mode at call time — no recompilation needed when switching resolutions.
Not verified in mode 3 (dual playfield): this library's stride/plane-count logic was written
for and only tested against mode 0/1. It has not been adapted or verified for mode 3, so widget
behaviour there is unsupported/undefined - do not rely on FillRect, DrawBox, DrawMsgBox, or
any other gui.s function while in dual-playfield mode.
- Including the library
- Primitive drawing
- Widget functions
- DrawBox
- DrawMsgBox
- DrawMsgBoxCaption ← captioned window
- DrawButton ← 3D gadget style
- DrawEditBox
- EditBoxProcessKey
- EditBoxPollKey
- DrawComboBox ← selectable list
- DrawWrappedText
- DrawGadget ← struct-based dispatch
- Mouse event manager
- GuiPollMouse
- GuiHitTestRect
- GuiHitTest ← struct-based
- GetGuiMouseX / GetGuiMouseY
- GADGET, EDITBOX, and COMBOBOX structs
- Button centering rules
- Sprite cursor integration
- Build integration
Assembly — include the offsets header in any .s that uses gadget structs:
include "gui.i"HAS — declare each function you call with extern func inside your code block:
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 DrawMsgBoxCaption(x:int, y:int, w:int, h:int, bg:int, border:int, caption: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 DrawComboBox(gadget_ptr:int) -> int;
extern func GuiPollMouse() -> void;
extern func GuiHitTestRect(x:int, y:int, w:int, h:int) -> int;
extern func GetGuiMouseX() -> int;
extern func GetGuiMouseY() -> int;
Fills an axis-aligned rectangle with a solid palette colour.
Handles partial left/right bytes via bit-masks. Mode-aware (5 or 4 planes).
| Argument | Meaning |
|---|---|
x |
Left pixel |
y |
Top pixel |
w |
Width in pixels |
h |
Height in pixels |
color |
Palette index 0–31 |
Returns d0 = 0.
Draws a horizontal 1-pixel line by calling FillRect(x, y, len, 1, color).
Draws a vertical 1-pixel line by calling FillRect(x, y, 1, len, color).
DrawBox(x, y, w, h, bg, border) -> intFills the rectangle with bg, then draws a 1-pixel uniform border in border on all four sides.
Used internally by DrawMsgBox.
DrawMsgBox(x, y, w, h, bg, border, str, tc) -> intDraws a bordered window and renders word-wrapped text inside it.
- Interior text area has a 1-character (8 px) padding on all sides.
- Text overflows are truncated to
max_cols × max_rowswheremax_cols = w/8 - 2andmax_rows = h/8 - 2. - Line-breaks at word boundaries; long words are hard-broken at the column limit.
- This function is a compatibility wrapper around
DrawMsgBoxCaptionthat passescaption_ptr = 0.
| Argument | Meaning |
|---|---|
x, y |
Top-left corner in pixels |
w, h |
Dimensions in pixels (multiples of 8 recommended) |
bg |
Background fill palette index |
border |
Border line palette index |
str |
Pointer to null-terminated message string |
tc |
Text colour palette index |
DrawMsgBoxCaption(x, y, w, h, bg, border, caption, str, tc) -> intDraws a bordered window with an optional title caption bar, then word-wrapped body text.
- When
caption != 0, a caption string is rendered in the top border row (clipped tow/8 - 2characters). - Body text behaviour is identical to
DrawMsgBox— word-wrapped, 1-character padding. - Passing
caption = 0is equivalent to callingDrawMsgBox.
| Argument | Meaning |
|---|---|
x, y |
Top-left corner in pixels |
w, h |
Dimensions in pixels (multiples of 8 recommended) |
bg |
Background fill palette index |
border |
Border line palette index |
caption |
Pointer to null-terminated caption string, or 0 for none |
str |
Pointer to null-terminated body message string |
tc |
Text colour palette index |
extern func DrawMsgBoxCaption(x:int, y:int, w:int, h:int, bg:int, border:int, caption:int, str:int, tc:int) -> int;
// Short caption fits within window width:
result = DrawMsgBoxCaption(16, 24, 288, 64, 8, 1, &my_title, &my_text, 1);
// Long caption is clipped automatically:
result = DrawMsgBoxCaption(16, 104, 200, 64, 4, 5, &long_title, &my_text, 1);
See example: examples/msgbox_caption_demo.has
DrawButton(x, y, w, h, bg, border, str, tc) -> intDraws a 3D raised button gadget with a centred label.
Rendering order:
FillRect(x, y, w, h, bg)— flat background face- Top edge (1 px) and left edge (1 px) in
bordercolour → bright highlight (raised effect) - Bottom edge (1 px) and right edge (1 px) in colour 0 (black) → dark shadow (raised effect)
- Label text centred horizontally and vertically (see Button centering rules)
The border argument is therefore the highlight colour (typically white/bright) while the shadow is always palette index 0 (black).
Recommended palette pattern:
| Index | Colour | Role |
|---|---|---|
bg |
mid-grey $444 |
Button face |
border |
white $FFF |
Top/left highlight |
0 |
black $000 |
Bottom/right shadow (automatic) |
tc |
white $FFF |
Label text |
DrawEditBox(x, y, w, h, bg, border, text_ptr, tc, cursor_pos, cursor_vis) -> intDraws a single-line text input field with optional caret.
Rendering order:
- Draws background + 1-pixel border (
DrawBox). - Draws visible text with 8-pixel left/right padding.
- If
cursor_vis != 0, draws a 1-pixel vertical caret atcursor_pos.
Behavior notes:
- Text is left-aligned and vertically centered.
- Horizontal scrolling is automatic when text grows beyond visible width.
visible_cols = (w - 16) / 8scroll_start = max(0, cursor_pos - visible_cols + 1)
text_ptrmust 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(text_ptr, max_len, cursor_pos_ptr, scancode) -> intProcesses one raw keyboard scan code and updates the edit buffer/cursor.
- Ignores key-release events (
bit 7set in scan code). - Maintains NUL-terminated buffer integrity.
- Supports printable insertion, backspace delete, left/right cursor movement.
Handled control keys:
$41Backspace$44Return$43Keypad Enter$45Escape$4FLeft arrow$4ERight arrow
Return codes:
| Return | Meaning |
|---|---|
0 |
No change / key-up / unhandled key |
1 |
Buffer or cursor changed |
2 |
Return (submit) |
3 |
Escape (cancel) |
EditBoxPollKey(text_ptr, max_len, cursor_pos_ptr) -> intConvenience wrapper around EditBoxProcessKey:
- Reads
current_keyfromlib/keyboard.s - Clears it (consumes event)
- Calls
EditBoxProcessKey - Returns the same
0/1/2/3status code
Use this when the edit box is the sole keyboard consumer in the frame loop.
DrawComboBox(gadget_ptr) -> intDraws a selectable list widget from a COMBOBOX struct. Items are stored as a single semicolon-separated, NUL-terminated string:
"Item A;Item B;Item C",0
Rendering behaviour:
- Each item is drawn on its own row within the widget bounds, clipped to the widget width.
- The selected item row is filled with
COMBOBOX_SELBGand text inCOMBOBOX_SELTEXT. - Unselected rows use
COMBOBOX_BGandCOMBOBOX_TCOLOR. - A 1-pixel border frame is drawn using
COMBOBOX_BORDER. - Rendering stops when the string ends or screen bottom is reached.
- Click detection: use
GuiHitTestRectper row to updateCOMBOBOX_SELECTED.
| Argument | Meaning |
|---|---|
gadget_ptr |
Pointer to a COMBOBOX struct (see COMBOBOX struct) |
extern func DrawComboBox(gadget_ptr:int) -> int;
// Preferred: go through the generic dispatcher
extern func DrawGadget(gadget_ptr:int) -> int;
call DrawGadget(&my_combo);
See example: examples/combobox_demo.has
DrawWrappedText(cx, cy, max_cols, max_rows, str, color) -> intLow-level word-wrap renderer. Arguments are in character units (1 unit = 8 pixels).
cx,cy— top-left character column/row of the text areamax_cols,max_rows— maximum columns and rows to usestr— pointer to null-terminated stringcolor— text palette index
Called internally by DrawMsgBox; use directly when you need precise text placement.
DrawGadget(gadget_ptr) -> intStruct-based dispatch: reads GADGET_TYPE from the struct and calls the appropriate renderer.
GADGET_TYPE |
Value | Renders via |
|---|---|---|
GADGET_TYPE_MSGBOX |
0 | DrawMsgBox |
GADGET_TYPE_BUTTON |
1 | DrawButton |
GADGET_TYPE_EDITBOX |
2 | DrawEditBox |
GADGET_TYPE_COMBOBOX |
3 | DrawComboBox |
Unknown types are silently skipped (returns 0).
See GADGET, EDITBOX, and COMBOBOX structs below for field layouts.
The GUI library maintains an accumulated absolute mouse position (gui_abs_mouse_x, gui_abs_mouse_y) that maps hardware delta movement to pixel coordinates clamped to the current screen size.
Call once per frame, after ReadMouse() from lib/input.s.
GuiPollMouse() -> void- Reads
GetMouseDX()/GetMouseDY()and accumulates intogui_abs_mouse_x/gui_abs_mouse_y. - X clamped to
0..319(mode 0) or0..639(mode 1); Y clamped to0..255. - Reads
GetMouseLBtn()and performs a leading-edge detect → writes 1 togui_lbtn_edgeon the first frame the button is pressed (held = 0). - Must be called after
ReadMouse()each frame.
GuiHitTestRect(x, y, w, h) -> intReturns 1 if all of these are true:
gui_lbtn_edge == 1(button just pressed this frame)- The accumulated mouse position is inside the pixel rectangle
(x, y, w, h)
Returns 0 otherwise.
Use this for inline buttons declared directly in HAS (no GADGET struct required).
if (GuiHitTestRect(100, 232, 120, 24) == 1) {
// Exit button clicked
}
GuiHitTest(gadget_ptr) -> intSame click detection as GuiHitTestRect but reads position and size from a GADGET struct.
GetGuiMouseX() -> int
GetGuiMouseY() -> intReturn the current accumulated absolute mouse position as a signed long.
Use these to feed the position to a hardware sprite cursor:
var mx: int = GetGuiMouseX();
var my: int = GetGuiMouseY();
call SetSpritePosition(0, mx, my);
No stack frame or arguments — equivalent to the GetMouseX/Y pattern in lib/input.s.
Defined in lib/gui.i. Size = 20 bytes.
| Field | Offset | Type | Meaning |
|---|---|---|---|
GADGET_X |
0 | word | Screen X position (pixels) |
GADGET_Y |
2 | word | Screen Y position (pixels) |
GADGET_W |
4 | word | Width (pixels, multiples of 8 recommended) |
GADGET_H |
6 | word | Height (pixels, multiples of 8 recommended) |
GADGET_BG |
8 | word | Background fill palette index |
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_EDITBOX=2) |
Assembly allocation example:
include "gui.i"
my_button:
dc.w 100 ; GADGET_X
dc.w 232 ; GADGET_Y
dc.w 120 ; GADGET_W
dc.w 24 ; GADGET_H
dc.w 8 ; GADGET_BG (mid-grey)
dc.w 1 ; GADGET_BORDER (white highlight)
dc.l btn_text ; GADGET_TEXT
dc.w 1 ; GADGET_TCOLOR (white text)
dc.w GADGET_TYPE_BUTTONDefined 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_ABORDERwhenEDITBOX_FLAGS bit0 == 1. - Caret visibility uses
EDITBOX_FLAGS bit1.
Defined in lib/gui.i. Size = 26 bytes. Offsets 0..19 are layout-compatible with GADGET, so DrawGadget dispatch works transparently.
GADGET_TEXT / COMBOBOX_TEXT must point to a semicolon-separated, NUL-terminated item list:
"Item A;Item B;Item C",0
| Field | Offset | Type | Meaning |
|---|---|---|---|
COMBOBOX_X |
0 | word | Screen X position (pixels) |
COMBOBOX_Y |
2 | word | Screen Y position (pixels) |
COMBOBOX_W |
4 | word | Width (pixels) |
COMBOBOX_H |
6 | word | Height (pixels) |
COMBOBOX_BG |
8 | word | Normal row background palette index |
COMBOBOX_BORDER |
10 | word | Frame colour palette index |
COMBOBOX_TEXT |
12 | long | Pointer to semicolon-separated item list |
COMBOBOX_TCOLOR |
16 | word | Normal row text palette index |
COMBOBOX_TYPE |
18 | word | Must be GADGET_TYPE_COMBOBOX (3) |
COMBOBOX_SELECTED |
20 | word | Selected item index (-1 = none) |
COMBOBOX_SELBG |
22 | word | Selected row background palette index |
COMBOBOX_SELTEXT |
24 | word | Selected row text palette index |
HAS struct allocation example:
bss demo_bss:
struct combo[1] {
x.w, y.w, w.w, h.w,
bg.w, border.w, text.l, tcolor.w,
type.w, selected.w, selbg.w, seltext.w
}
DrawButton uses pixel-level centering formulas snapped to the 8-pixel character grid:
Horizontal — cx = (x + (w − label_px) / 2) / 8
The label pixel width is strlen(label) × 8. The formula computes the exact pixel midpoint, then snaps to the nearest character column. Produces perfect centering whenever (w − label_px) is a multiple of 16.
Vertical — cy = (y + h/2) / 8
Rounds the button's vertical midpoint to the nearest character row.
Centering quality by button height:
| Button height | Inner area | Gap each side | Visual result |
|---|---|---|---|
| 16 px | 14 px | ~3 px (not exact) | Acceptable |
| 24 px | 22 px | 7 px each | Perfect ✓ |
| 32 px | 30 px | ~11 px | Good |
| 40 px | 38 px | 15 px | Good |
Recommendation: use
h = 24(or any multiple of 8 ≥ 24) for perfectly centred button labels.
Ath = 16text is placed in the lower half of the button face with a slight visual asymmetry.
A hardware sprite can serve as a pixel-precise mouse cursor. The full pattern is:
// --- Declare sprite functions (lib/sprite.s) ---
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;
// --- Declare GUI mouse accessors ---
extern func GetGuiMouseX() -> int;
extern func GetGuiMouseY() -> int;
// --- One-time initialisation (after SetGraphicsMode) ---
result = CreateSprite(0, &cursor_data); // copy fast→chip RAM; store palette
call ApplySpritePalette(0); // write COLOR17–19 into copper list
call ShowSprite(0); // mark slot visible
call SetSpritePosition(0, 160, 128); // initial position
// --- Per-frame update (inside VBlank loop) ---
call ReadMouse();
call GuiPollMouse();
var mx: int = GetGuiMouseX();
var my: int = GetGuiMouseY();
call SetSpritePosition(0, mx, my);
call UpdateCopperList();
Cursor sprite data layout (data section — fast RAM is fine; CreateSprite copies to chip RAM):
data cursor_data:
// 4 palette words immediately before the sprite label (required by CreateSprite)
// offsets -8/-6/-4/-2 relative to the sprite data label:
// -8 = color0 (transparent), -6 = color1, -4 = color2, -2 = color3
cursor_pal.w = $000, $FFF, $CCC, $888 // transparent, white, light-grey, mid-grey
// Sprite data: height, ctrl0, ctrl1, (plane0, plane1) × height, 0, 0 terminator
// All rows use plane0=shape, plane1=0 → pixels select color1 (white).
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
Pass &cursor (the address of the height word) to CreateSprite.
ApplySpritePalette(0) writes cursor_pal colors 1–3 into the copper list at COLOR17–COLOR19 (sprite 0/1 pair in lores mode).
Link lib/gui.s, lib/gui_keyboard.s, lib/sprite.s (if using cursor), lib/input.s, lib/graphics.s, lib/font8x8.s, lib/helpers.s, and lib/takeover.s together:
# 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:
python -m hasc.cli examples/msgbox_demo.has -o build/msgbox_demo.s
vasmm68k_mot -Fhunk -devpac -I lib/ -o build/msgbox_demo.o build/msgbox_demo.s
vasmm68k_mot -Fhunk -devpac -I lib/ -o build/gui.o lib/gui.s
vasmm68k_mot -Fhunk -devpac -I lib/ -o build/gui_keyboard.o lib/gui_keyboard.s
# ... assemble the other libs ...
vlink -bamigahunk build/msgbox_demo.o build/gui.o build/gui_keyboard.o build/graphics.o \
build/sprite.o build/font8x8.o build/helpers.o build/takeover.o \
build/input.o -o build/msgbox_demo.exeSee examples/msgbox_demo.has and examples/editbox_demo.has for complete working demos.