A simple AmigaOS GUI in assembler

A simple AmigaOS GUI in assembler

A couple of months ago I wrote a tutorial on how to make a GUI in C, which was published (in English) on the Amiga-News.de website. This tutorial also fitted in as an example for my first book The complete source of the example can be downloaded from the downloads page. Since the book is also intended for assembly programmers I decided to produce a similar example source to do the same GUI related things in assembly. This post is not as much a tutorial but more a accompanying text to the assembly source, which has also been made available on the download page.

The Example

To keep in line with the book, the example uses the Gadtools library for the GUI and requires a Kickstart 2.x ROM or newer. It uses the same V3.9 NDK (Native development Kit) and setup as the book and has been tested with Asm-Pro as well as HiSoft DevPac.

Since the NDK does not contain a header file for the gadtools.library itself I have added the library offsets for the various Gadtools functions at the top of the source file. This is the main reason why the Gadtools functions do not start with the _LVO prefix, while all other OS related functions do.

Rather than reproducing the whole source here, I will highlight interesting parts of the source to further explain what they do. Please note that the sources have not been optimised for speed or size since I wanted to maintain readability. So instead of juggling various registers to keep as much data in the CPU as possible I chose to store and retrieve stuff from labelled memory locations to make it easier to follow.

Opening the window

The window is opened using the OpenWindowTagList() function, which is part of the Intuition library. This function was introduced with Kickstart 2.0 (V36).

OpenWindow:     MOVE.L   IntBase(PC),a6      ; a6 = IntBase
       SUB.L    a0,a0            ; APTR NewWindow = null
       LEA.L    .Tags(PC),a1        ; APTR taglist
       JSR    _LVOOpenWindowTagList(a6)
       MOVE.L   d0,MainWin        ; Sets Z-flag when null
       RTS

.Tags   DC.L    WA_Width,200
        DC.L    WA_Height,100
        DC.L    WA_Title,.Title
        DC.L    WA_IDCMP,IDCMP_CLOSEWINDOW|IDCMP_MENUPICK|IDCMP_RAWKEY|IDCMP_REFRESHWINDOW|BUTTONIDCMP|CYCLEIDCMP
        DC.L    WA_DragBar,1
        DC.L    WA_DepthGadget,1
        DC.L    WA_Activate,1
        DC.L    WA_CloseGadget,1
        DC.L    WA_NewLookMenus,1
        DC.L    TAG_END

.Title  DC.B    "Hello World!",0

After opening the window, a the pointer to the Window structure is stored in the MainWin location so it can be used later. This action also sets the Z-flag so that the calling function can check for errors and exit the program if for some reason the window could not be opened.

Creating and adding the menu

For the creation of the menu bar the Gadtools library is used. This makes menu creation much simpler compared to using only the functionality provided by Intuition. As an added bonus Gadtools will also calculate all locations and sizes depending on the selected font.

This happens in three distinct steps. The first is the call to CreateMenu(), which converts the array of Gadtools' NewMenu structs into a linked list of Intuition's Menu and MenuItem structs. This is followed by the call to LayoutMenus(), which will calculate the sizes and locations of all the elements in the menu. The last step is to actually add the menu to the window and this is done by calling the Intuition function SetMenuStrip().

CreateMenu:    MOVE.L    GadBase(PC),a6        ; a6 = GadBase
        LEA.L    .NMArray(PC),a0       ; APTR NewMenu array
        SUB.L    a1,a1            ; APTR Taglist = null
       JSR    CreateMenus(a6)
        MOVE.L    d0,WinMenu        ; APTR Intuition menu
       BEQ.B    .NoMenu            ; Null means error

        ; Set sizes and locations in menu
        MOVE.L   d0,a0            ; APTR Menu
        MOVE.L   VisualInfo(PC),a1    ; APTR VistualInfo
        SUB.L    a2,a2            ; APTR Taglist = null
        JSR    LayoutMenus(a6)
        TST.L    d0            ; ULONG Success
        BEQ.B    .NoMenu            ; Skip if there was an error

        ; Attach the menu to the window
       MOVE.L    IntBase(PC),a6        ; a6 = IntBase
        MOVE.L    MainWin(PC),a0        ; APTR Window
        MOVE.L    WinMenu(PC),a1        ; APTR Menu
        JSR     _LVOSetMenuStrip(a6)
.NoMenu        RTS

The address of the menu is stored in WinMenu. It will be needed later to free the menu from memory and to update the contents of the menu while the program is running. For brevity I've left the large array of NewMenu structs out here.

Creating and adding gadgets

Creation of gadgets with Gadtools uses a different method compared to the way Gadtools creates menus. Instead of providing an array of structs and calling the creation function once we now need to call the creation function for each individual gadget separately. The CreateGadget() function also needs the pointer to the previous gadget so that it can create a linked list. To get a "previous" pointer for the very first call to CreateGadget() we need to call CreateContext().

CreateGadgets: MOVE.L    GadBase(PC),a6        ; a6 = GadBase
       LEA.L    WinGGList(PC),a0    ; APTR to gadget list
     JSR    CreateContext(a6)
     MOVEA.L   d0,a0            ; APTR Gadget
TST.L d0 ; Is the result null?
     BEQ.B    .ErrOut            ; Yes. That is an error

To make it possible to use a loop to create the gadgets I create a simple table with the "kind", the NewGadet pointer and the taglist pointer for each gadget like this:

.GGArray       DC.L      BUTTON_KIND,.QuitGG,0
       DC.L    BUTTON_KIND,.AboutGG,0
       DC.L     CYCLE_KIND,.OptionsGG,.OptionsTL
        DC.L    0

The code that creates the gadgets then keeps calling CreateGadget() until it reaches the kind of 0 that indicates the end of the table. Later on some of the individual gadget pointers may be required to update the status during the running of the program. For this purpose the MyGadgets array is used in the routine below.

               MOVE.L    VisualInfo(PC),d7
        LEA.L     .GGArray(PC),a5       ; APTR information array
        LEA.L     MyGadgets(PC),a4    ; APTR GG Pointer storage
.NextGG        MOVE.L    (a5)+,d0        ; ULONG Kind
        BEQ.B    .Done           ; Zero kind? End of array
       MOVE.L    (a5)+,a1         ; APTR NewGad
        MOVE.L    d7,gng_VisualInfo(a1) ; Place VisualInfo
        MOVE.L    (a5)+,a2        ; APTR taglist
             JSR    CreateGadget(a6)
             TST.L    d0            ; Result is null?
             BEQ.B    .ErrOut            ; Yes. bail out
             MOVEA.L   d0,a0            ; APTR Gadget
             MOVE.L    a0,(a4)+        ; Store pointer for later
             BRA.b    .NextGG

The start of the linked list of gadgets is stored in the location named WinGGList. This pointer is later needed to free all the gadgets from memory. Luckily that only takes a single call and does not require each gadget to be freed individually.

The WinGGList pointer is also used to attach the gadgets to the window, which is a two step action. First the list is added to the window and then the list is refreshed. This last step ensures that all gadgets show the correct state.

.Done          MOVE.L    IntBase(PC),a6        ; a6 = IntBase
            MOVE.L    MainWin(PC),a0        ; APTR Window
            MOVE.L    WinGGList(PC),a1     ; APTR Gadget List
            MOVEQ    #0,d0            ; UWORD Position
            MOVEQ    #-1,d1             ; WORD Numgad (-1 = all)
            JSR    _LVOAddGList(a6)

            MOVE.L    WinGGList(PC),a0    ; APTR Gadget List
            MOVE.L    MainWin(PC),a1        ; APTR Window
            MOVEQ    #-1,d0            ; WORD Numgad (-1 = all)
            JSR    _LVORefreshGList(a6)
.ErrOut        RTS

Processing messages

When the user interacts with the elements of the window (e.g. selects a menu or clicks a gadget) an IDCMP message is sent to the message port of the window. It is up to the program to wait for these messages. This can be done with the Exec function WaitPort() which puts the program to sleep until a message arrives on the port.

.WaitMsg       MOVE.L    4.W,a6                ; a6 = ExecBase
        MOVE.L    .WinPort(PC),a0       ; APTR MsgPort of window
        JSR     _LVOWaitPort(a6)

When the call to WaitPort() returns the message can be retrieved and examined. The Window has Gadtools gadgets, which means that the Gadtools message functions must be used to retrieve and reply the message. This goes as follows:

.NextMsg       MOVE.L    GadBase(PC),a6        ; a6 = GadBase
       MOVE.L    .WinPort(PC),a0       ; APTR msgPort
        JSR     GT_GetIMsg(a6)
        MOVE.L    d0,.Msg          ; APTR IntuiMessage
        BEQ.B    .Loop            ; None? Wait for the next one

Please note that when WaitPort() returns multiple messages could be waiting on the port. It is therefore important to continue to retrieve and process messages and only call WaitPort() again after GT_GetIMsg() has returned null indicating that no more messages remain.

Also important to note is that with Gadtools there may be messages on the port that are for the internal use of Gadtools only. In that case the call to GT_GetIMsg() will either return the next message (that is not internal to Gadtools) or NULL. This means that it is not impossible for the first call to GT_GetIMsg() to return NULL.

The im_Class member of the message will indicate the class of message that has been received. This could for example be IDCMP_MENUPICK for user menu interaction or IDCMP_GADGETUP for user gadget interaction.

After the program has finished with the contents of the message it must be replied to the system so that its memory can be freed.

              MOVE.L    GadBase(PC),a6        ; a6 = GadBase
MOVE.L .Msg(PC),a1 ; APTR IntuiMessage
             JSR       GT_ReplyIMsg(a6)

After calling GT_ReplyIMsg() the contents of the message should not be touched again.

Processing gadget messages

When the user interacts with a gadget the message received on the port will be of the IDCMP_GADGETUP class. The im_IAddress member of the message will contain the address of the gadget the user interacted with. This can then be user to read the gg_UserData field of the gadget struct.

.GadgetUp      MOVE.L    im_IAddress(a1),a0    ; APTR originating Gadget
        MOVE.L    gg_UserData(a0),d0    ; ULONG Command
       CMPI.W    #CMD_OPTIONS,d0       ; Was it the options gadget?
        BNE.B    .NotOptionsGG        ; No. Skip the next part
       ADD.W    im_Code(a1),d0        ; Add selected option ID.
.NotOptionsGG  ADDI.W    #SRC_GADGET,d0        ; Add source of command
        MOVE.W    d0,Command         ; Set the command   
       BRA.B    .MsgDone

For cycle gadgets Gadtools places the ID of the user's new current selection in the im_Code field of the message.

Processing menu messages

Menu interaction causes IDCMP_MENUPICK messages to be received on the port. Unfortunately the Menu and MenuItem structs do not contain a UserData field. To get around this issue Gadtools will store the user data for each menu and item directly after the Menu or MenuItem struct. The im_Code field of the message will contain the menunumber of the menu or item that the user interacted with, which can be used with the ItemAddress() function of Intuition to get the address of the Menu or MenuItem struct.

.MenuPick      MOVE.W    im_Code(a1),d0        ; UWORD Menu number
        BEQ.B    .MsgDone         ; Zero? No menu
        MOVE.L    WinMenu(PC),a0        ; APTR Menu
        MOVE.L    IntBase(PC),a6        ; a6 = IntBase
        JSR    _LVOItemAddress(a6)
        TST.L    d0             ; Check the resulting address
        BEQ.B    .MsgDone        ; Null? No menu found
        MOVEA.L   d0,a0             ; APTR MenuItem
        MOVE.L    mi_SIZEOF(a0),d0    ; Userdata stored after struct
        ADDI.W    #SRC_MENU,d0        ; Add source of command
        MOVE.W    d0,Command        ; Store command
       BRA.B    .MsgDone

For this example the only menu elements that will produce a message are menu items. This means that the addresses returned by ItemAddress() will always be of a MenuItem struct (and never of a Menu struct). This makes calculating the end of the struct to find the userdata quite straightforward.

Updating the window

The example program has an "Options" menu item as well as an "Options" cycle gadget. When the user changes the selection on one it is up to the program to update the selection on the other. Of course this is just a trivial example, but it shows how a program can update its menu and its gadgets to reflect changes in its internal state.

Updating the cycle gadget

Updating the cycle gadget is straightforward thanks to the Gadtools GT_SetGadgetAttrs() function. It takes a taglist with the new attributes in which we use the GTCY_Active tag to change the ID of the gadget's current selection.

The "Command" contains the source of the command (gadget or menu), the command itself and in the case of CMD_OPTIONS the command will also contain the ID of the new selection. To update the gadget all we need is the option ID.

               MOVE.W    Command(PC),d7           ; UWORD Command, Source + Option
MOVE.L    MyGadgets+8(PC),a0    ; APTR Cycle gadget
        MOVE.L    MainWin(PC),a1        ; APTR Window
        SUB.L     a2,a2             ; APTR Requester = null       
        LEA.L    .UpdateTags(PC),a3    ; APTR Taglist
       ANDI.W    #$F,d7             ; UBYTE Keep only the option ID
        MOVE.W    d7,6(a3)         ; Place in taglist
        JSR    GT_SetGadgetAttrs(a6)
        RTS

.UpdateTags:   DC.L    GTCY_Active,0,TAG_END

Updating the menu

Gadtools unfortunately does not provide any convenience functions for updating the menu. Before any part of the menu can be updated it needs to be removed from the window with the Intuition function ClearMenuStrip(). Then the menu can be updated by modifying the structures that make up the menu. After that the menu can be added to the window again by calling ResetMenuStrip().

Only the "options" part of the menu needs to be updated. The MenuItem structs that are part of it can be found using the menunumber of the "options" menu:

               MOVEQ     #1,d0                  ; UWORD Menunumber
       MOVE.L    WinMenu(PC),a0        ; APTR Menu
        JSR    _LVOItemAddress(a6)

The address found points to the top of the three menu items that make up the "Options" menu. Only the selected item should have the CHECKED flag set. The following routine loops over the MenuItem structs and sets the flag for the correct item and removes it from all the other ones. D7 already contains the command + option code.

.NextItem      MOVE.L    d0,a0                  ; APTR MenuItem
       TST.L    d0            ; Null?
       BEQ.B    .MenuEnd        ; Yes. No more items
        MOVE.L    mi_SIZEOF(a0),d1    ; Get the item's command
        CMP.L     d7,d1             ; Same as current command?
        BEQ.B     .SetCheck
        ANDI.W    #~CHECKED,mi_Flags(a0) ; Clear the CHECKED flag
        BRA.B    .CheckDone
.SetCheck     ORI.W    #CHECKED,mi_Flags(a0)  ; Set the CHECKED flag.
.CheckDone     MOVE.L    mi_NextItem(a0),d0    ; APTR Next MenuItem
       BRA.B    .NextItem        ; Keep processing.

Wrapping Up

There is a bit more functionality in the full source code than I've highlighted here. There is for example also the use of the "Esc" key to close the window. And there is an "About" function that opens a simple requester. My C tutorial that was published on the Amiga-News.de website contains more information on why certain things are done in a particular way - and these things are also true for this assembly example so you may want to give that a read as well.

Posted on