The C64 OS Programmer's Guide is being written

This guide is being written and released and few chapters at a time. If a chapter seems to be empty, or if you click a chapter in the table of contents but it loads up Chapter 1, that's mostly likely because the chapter you've clicked doesn't exist yet.

Discussion of development topics are on-going about what to put in this guide. The discusssions are happening in the C64 OS Community Support Discord server, available to licensed C64 OS users.

C64 OS PROGRAMMER'S GUIDE

Writing an Application: Tutorial 2: TestGround

Part 1: Initialization

In this next tutorial, things are quickly going to get a lot more complicated. You should familiarize yourself with everything in Tutorial 1: Hello World first, because this tutorial will not go into detail on anything that was already covered there.

Several new concepts are added to this example Application, TestGround, and each of these new ideas will be examined and explained. These include:

  • Toolkit-based user interface
  • Toolkit subclassing
  • Toolkit environment
  • pointer and string stores
  • class delegation
  • loading, linking and unloading shared libraries
  • loading and saving config or state information
  • the Application file reference
  • mouse and keyboard event handling
  • custom keyboard shortcuts
  • memory allocation
  • responding to changes of the theme and other system flags
  • supporting the status bar
  • loading custom bundle resources, and
  • the initialization binary

This is a long list of new abilities; some of them may be a bit complex. More information about how each of these works can be found in other parts of this guide and links are provided to these additional explanations.

Hello World was very simple, providing the bare bones of a launchable Application. TestGround does not cover everything in C64 OS, but it does make use of substantially more concepts and attempts to incorporate many more C64 OS technologies. This Application can be used to test various features of the KERNAL, Toolkit and libraries.

In order to tackle this set of new concepts, this tutorial is divided into two parts. The first part is about Application initialization and building the user interface. The second part (TestGround Part 2: Main Application) is about the Application's main business logic.



The Bundle Contents

Let's begin with an overview of the bundle contents. The common and required components are there, plus some additional components we are not yet familiar with.

The about.t metadata file is present and configured. The 3icon file, icon.charset, is present along with the icon.charset.a file used to generate this icon. The menu definitions file, menu.m, is present with more menus and items. The main binary, main.o, is there, as required. And its source code, main.a.

config.i

In addition, there is a config.i file. This file, like all files in C64 OS that end with .i, is a self-generating file for state data. It is in a binary format. It's safe to delete this file if it were ever to become corrupt or out of date, the Application will re-generate it automatically. The reset button in the "About This App" Utility scratches this file and other .i files from the Application bundle.

init.o (init.a)

A new binary file, init.o, and its source code, init.a, are present. This is an optional second binary that the loader searches for and loads in if it's present. It allows us to stash code that is only required to initialize the Application into a temporary binary that does not remain in memory during the lifetime of the Application.

main.l and main.xl

Two files, main.l and main.xl, are used during development to link the main.o and init.o binaries together.

sample.t

Another file, sample.t, is a generic non-code bundle resource that is loaded in for the Application to present to the user.

tkfooter.r (tkfooter.a)

And lastly, tkfooter.r, and its source code, tkfooter.a, is a custom Toolkit subclass. This class file is loaded in and linked to its superclass, and then instantiated and added to the user interface, just to show how this is done.

The Menu Definitions File

Without a complete recap of how to create the menu definitions file, let's examine the structure of this menu.m in TestGround.

System; is a menu header (identified by the ";") followed by "c" to indicate it has 3 immediate children. Raise Exception, a spacer, and Go Home.

Immediately following, since it's not a blank line, is Mouse; another menu header. The "d" indicates that this has 4 immediate children; options used for testing KERNAL calls related to the mouse pointer.

Lastly, there is the View; menu header, with "f" to indicate 6 immediate children. A triplet of items: Table, List and Text, as we will see, mirror the three tabs in the main interface and are used to provide a place to show the user that keyboard shortcuts (COMMODORE+1, COMMODORE+2, and COMMODORE+3) can be used to change tabs. The triplet is separated by a spacer, and then two options affecting other user interface elements.

Following the 6 immediate children of the View menu is a blank line, indicating the end of the menu definitions file.

Split Binaries: Linking the init and main binaries

Before delving into how the main binary is structured, we have to discuss how and why the code for this Application is split into two different binaries. We will look at how they are able to work together and how they are assembled.

main.o and other sources of functionality

The main.o binary is required. As we saw in the Hello World tutorial, the bundle will not launch if main.o is missing. The loader loads in main.o, which is assembled to appbase and must begin with the Application's vector table. The code in main.o constitutes the main runtime business logic or domain-specific logic of the Application.

This is not the only code that makes the Application fulfill its purpose. The most obvious other source of logic is the C64 OS KERNAL (and the KERNAL ROM), used for the most low-level, most ubiquitous functionality. The KERNAL is a permanent backbone upon which Applications can always rely.

Additionally, code can be found in shared libraries. Libraries contain code that is not specific to your Application and could therefore usefully be shared by many different Applications. Code in libraries is not sufficiently ubiquitous that it merits taking up precious memory space at all times. The Application needs to load and unload libraries in order to ensure that they are available in memory for use. It is common for an Application to load the libraries it needs during initialization and to unload the libraries during teardown. However, this is not the only option. Loading of a library may be deferred until it's needed. Libraries may be unloaded, temporarily, in response to a low-memory warning message, or, in some circumstances, a library may be loaded, used for a certain purpose, and unloaded immediately thereafter.

Another source of functionality comes in Toolkit classes. Each class neatly bundles up the behaviors, both drawing and event handling, so that this logic is not mixed into the Application's main business logic.

init.o

Lastly, there is a special kind of logic that an Application requires for initialization. Initialization often includes: Allocating memory for a screen layer buffer, pushing the screen layer, loading shared libraries, loading and linking custom toolkit classes, instantiating classes into objects, customizing those objects and appending them together into a user interface node tree, and hooking up delegates, datasources and callbacks.

In a complex Application, all of this work can amass a non-trivial amount of code. However, due to its nature as initialization, once this code has run it never needs to be run again. It makes little sense for it to remain in memory, taking up space, throughout the lifetime of the Application. For this reason, all code that is strictly initialization-only can be moved to an optional second binary, called, init.o.

When the loader loads in an Application, first it loads in main.o and marks that memory as allocated by the Application. Then it checks for an init.o binary and loads it in temporarily, without marking that memory as allocated. The init.o binary should immediately follow the main.o binary, putting it low in memory. Memory allocations are performed from the top down. This is particularly important for allocations performed during initialization. When initialization is complete, the init.o code is abandoned and must never be called or relied upon again, as its memory space is liable to be allocated and overwritten by something else.

Relationship between main.o and init.o
Relationship between main.o and init.o

Assembling main.a and init.a together

There is only a single point of connection from main.o into init.o. When the KERNAL jumps through the Application vector table at the start of main.o, that vector can be pointed to the top of init.o. The address of the start of init.o can be determined by placing an init label at the very end of main.a.

The init.o binary, however, will have many links back to the main.o binary. In order for the code in init.a to know the addresses of routines and variables found in main.a, a label export file must be generated from main.a. When the labels are exported, however, all the labels from all of main.a's includes are found in that file. To strip away the labels not found within main.o, you can use the exlabel tool (//os/c64tools/:exlabel). Exlabel takes no input parameters; it reads in "main.l" from the current directory, parses it, and exports "main.xl" to the current directory, overwriting any pre-existing main.xl. The main.xl file can then be included by init.a.

Here is the order of operations:

  1. Create main.a source code file (*= appbase)
  2. Put a label, init, at the end of main.a
  3. Create init.a source code file (*= init)
  4. Put an include "main.xl" at the start of init.a
  5. Assemble main.a to main.o
  6. Export labels to main.l
  7. Load and run exlabel to generate main.xl
  8. Assemble init.a to init.o

Source Code

The label files, main.l and main.xl, are not needed by the Application at runtime. These two temporary files are only used during assembly and should be considered part of the source code of the Application.

If you use C64 Archiver to create a .CAR file of your Application bundle, the option to exclude "code" files puts "a,l,xl,i" into the skip extensions field. This allows you to roll up your Application bundle, while excluding main code files, label exports, and auto-generated state files.

Accurate label information is only available in TurboMacroPro immediately following assembly. If you make any changes to main.a, it is necessary to reassemble it, re-export main.l, run exlabel again, and then reassemble init.a. If you only make changes to init.a, it can safely be reassembled using the existing main.xl file.

When coding native on the C64 with TurboMacroPro, the steps for assembly are as follows: (with JiffyDOS commands, and assuming you've put the dev tools, such as tmp and exlabel, in the root directory.)

  1. Launch TMP (£//:tmp)
  2. Load in main.a (←l (the letter el) then enter "main.a")
  3. Assemble main.a (←3 then enter "@:main.o")
  4. Export labels (←u then enter "@:main.l")
  5. Quit TMP (←1 (the number 1))
  6. Load and run exlabel (↑//:exlabel)
  7. Launch TMP again (£//:tmp)
  8. Load in init.a (←l (the letter el) then enter "init.a")
  9. Assemble init.a (←3 then enter "@:init.o")

Because init.a has included all of the labels found within main.a by including main.xl, the initialization code can make liberal use of the addresses found within main.o, as though they are local addresses. We will see more examples of this as we explore and explain init.a.


The Application's Init Binary

Now that we know how the init.o binary is assembled, when it gets loaded, and the fact that the KERNAL will jump into the start of it because the Application's vector table points the appinit vector to the start of init.o, let's begin by exploring init.a.

The structure of init.a

Although the structure of init.a is much more linear than main.a, the code is still divided into a number of logical sections.

  • Headers and Includes
  • Data Structures and Content
  • Initialization Code
  • KERNAL and Library Externs

Headers and Includes

In this Application there are many more headers and includes than were present in Hello World. Again, in the interest of clarity the headers and includes are grouped according to what they are.

The very first include does not use an include macro, it brings in main.xl from the current directory, that is from the root of Application's bundle. This brings in all the labels from main.a including "init", the address to which init.a is assembled.

The includes in init.a are only for the labels and macros that will be used in init.a, main.a may have a different but overlapping set of includes. The first group are includes to labels for features of C64 OS that are not immediately tied to the KERNAL or Toolkit; app, ctxdraw, and pointer were seen already in Hello World. ctxcolors is different than colors. colors defines the VIC-II's color codes, ctxcolors defines the elements of the customizable color theme.

icons defines a set of numbered 1x1 character (8x8 pixel) icons that are provided by C64 OS to be used by Applications in common. Different Application's that want to show, for example, a trash can icon, can load that icon from the system's icon library. If the user customizes the icons in the OS's icon library, every Application that uses them will display the user's updated set. This, along with the color theme, gives C64 OS a great deal of flexibility to be skinned by the user.

The next group of includes are for C64 OS KERNAL modules, file, memory, screen, service, string and toolkit.

The following groups of includes are ones we have not encountered yet. These are for the Toolkit classes that our Application will use. The header file, classes, defines the numeric IDs of the 9 built-in classes. The class pointer is fetched using the classptr KERNAL call and supplying one of these IDs. The include, tksizes, is required for finding the method and property offsets of Toolkit objects.

The series of includes that use the macro inc_tkh bring in the method definitions for each of the classes whose methods we need to call. And the series of includes that use the macro inc_tks bring in the property definitions for the classes we use.


Following the includes, is the declaration of where init.a assembles to. It assembles to init, which as discussed earlier is defined by main.xl, which is the first address following the final byte of main.o. Since main.o's appinit vector points to here, there must be valid code here. However, for organizational purposes, it is preferable to keep the data structures and content variables at the top of the file, before the rest of the code. Therefore, the first line of code in this file is a JMP to a start label, which is set at the start of the code segment.

Data Structures and Content

The initialization has some data structures and content that are truly only required during initialization. For example, the names of the toolkit class files, the pointers to those class files, and the name of a text file that will be loaded in, these are needed for building the UI and bringing in the resources, but unless the Application needs to create new user interface classes at runtime, once the classes have been instantiated it is no longer necessary to keep a table of pointers to them, nor is it necessary to know what filenames were used to load them.

Most runtime loadable Toolkit classes are installed in the system's "tk" directory (//os/tk/). So we've defined tkpath as tk, to be able to reference that directory.

We're going to load 6 Toolkit classes, 5 of them are in the tk directory, and one of them is totally custom, tkfooter.r, and is stored in the Application bundle. After loading them, we need to keep a pointer to the start of each class. While it is entirely possible to make a label for each class, that uses a lot of labels and pollutes the label space. It is often more complicated to come up with well named labels than it is to use a single label for a table of pointers.

Therefore, we've defined three labels, each of which have 6 corresponding indexes. classes is the list of 6 class pointers. csizes is the list of sizes in pages of the 6 classes. cnames is a label to a special string fetching macro, strxyget. The macro, strxyget, is defined by the string include. Similar to the switch macro we saw in Hello World, strxyget prepends a table of null terminated strings with a short routine that makes it easy to retrieve a pointer to one of these strings by an index passed in the accumulator. We'll see how this gets used below.

Next is icontab, the icon table. The KERNAL call loadicns takes a pointer to a structured icon table. It then fetches the data from the icons directory (//os/icons/), and installs them into the character set. The table is structured as follows:

Charset Index EOR Bitmask Icon ID
$f7 $ff icn_table
$f8 $ff icn_list
$f9 $ff icn_text

The whole table must be terminated by a null byte (#$00), so that loadicns knows where the table ends. In the C64 OS Character Set, there are 18 characters reserved for customization; the final 9 characters of block 4, and the corresponding final 9 characters of block 8.

The table of screencodes and the 8 blocks can be found in the reference post, VIC-20 / Commodore 64 SuperChart. Blocks 1 to 4 have bit 7 low. Blocks 5 to 8 have corresponding but reversed characters from blocks 1 to 4, and have the same numeric index but with bit 7 high. Therefore, to find the reversed version of any low character, one only needs to set bit 7 of the low character's index.

If your Application does not need both standard and reversed versions of characters, you can use all 18 characters for different icons. However, if you do need standard and reversed versions of the same icon, you put the standard version in one of the 9 characters at the end of block 4, and the reversed version at the corresponding index in block 8.

All of the icons in the icon library are standard, not reversed. But the icon table includes an EOR bitmask. When the icon data is loaded, each of the 8 bytes is EOR'd with the bitmask before being written into the character set. Use a $00 to keep the standard icon, or use an $ff to reverse the standard icon.

In our table, we're going to bring in small icons for a table, a list and a text view, which will be shown on the tabs of the main user interface. We only need reversed versions of these, so the table has $ff in the bitmask for all three, and they are installed to the first 3 of 9 available character slots, in the block 8, the half of the character set typically used for reversed characters.

Lastly, we have a couple of variables used for bringing in the sample.t text file resource found in the Application's bundle.



Initialization

The code of init.a is all about initialization, so it is mostly a single long routine that simply does one thing after the next. There are a couple of minor exceptions, some helper routines are put near the end.

We'll walk through this code a section at a time and explain what each section is doing.

Allocating Memory



The zero page addresses, $fb, $fc, $fd, and $fe, are reserved as temporary workspace addresses. They are a common place to put two pointers, $fb/$fc and $fd/$fe. At the start we've defined ptr as $fb, which will also use $fc, as a generic pointer.

On the first line of the initialization code is the label, start. This is where the first line of code in this file JMPs to, so we were able to put the data structures and content above this.

We have already seen in Hello World how initextern is used to intialize the KERNAL link table. But we've got something new this time; initextern is called twice. The reason is because we have two KERNAL link tables. The first comes at the bottom of main.a, and lists all of the KERNAL calls that will be used during the runtime of the Application, these can also be used from init.a. The second KERNAL link table is found at the bottom of init.a and includes only KERNAL calls that are needed during initialization but not used during runtime. The label extern comes from main.xl, whereas the label iextern is found at the end of init.a.



Next we call killmouse. This isn't totally necessary, as the sprites are already disabled by JiffyDOS and by C64 OS during loads to accelerate disk access. However, since we have many things to load in, we would otherwise see the mouse pointer blinking on and off. It can look cleaner to turn the mouse pointer off during initialization and turn it back on at the end.



Hello World did not use a screen layer buffer, its draw context structure pointed directly at the C64 OS screen compositing buffers. This was acceptable only because the draw task of Hello World was incredibly simple. In TestGround, we are creating a large, complex, Toolkit-based user interface. Although it is technically possible to draw this directly to the screen compositing buffers, in practice it would be much too slow. Every time a menu closes and reveals an area of the screen layer below, the entire Toolkit UI would need to be redrawn. Opening and closing menus and move Utilities around would become noticeably sluggish.

Instead, our Application will allocate memory to serve as dedicated screen layer buffers just for our user interface. Our screen layer can later be composited into the operating system's compositing buffers, which is much faster. We need two buffers, 1000 bytes for character data and 1000 bytes for color data. Although this could be accomplished with a single call to pgalloc to allocate 8 pages (256 * 4 = 1024 bytes, 256 * 8 = 2048 bytes,) that would force the allocator to find 8 contiguous pages. Our two buffers do not need to be contiguous. And it is a good habit to get into making separate pgalloc calls for discontiguous allocations.

About Paged Memory Allocations

C64 OS has a paged memory allocation table. An Application has access to approximately 30 kilobytes of memory for code and data, which works out to around 120 pages of memory.

Different processes, at different times, request blocks of memory in page-sized increments. The allocator searches for a block of pages that are available together, and returns the address of the first page of the block. Searches are performed from the top of memory down.

Imagine that pages $80,$7F,$7E and $7D are available, but $7C is occupied by something like a file reference, a shared library or a driver. If you request 8 pages in a single call to pgalloc, it will not be able to use those first 4 available pages, because $7C splits those 4 pages from the next available 4 pages. If you can make multiple smaller allocations, those are always preferable to making fewer larger allocations, because they can fit more easily into what may eventually become a jigsaw puzzle of allocated memory.

After the first allocation of 4 pages, we write the page address into the draw context's color buffer origin's high byte. The next 4 page allocation, we write the page address into the draw context's character buffer origin's high byte. The drawctx is in main.a.

We need one more buffer, for text data. Our user interface will give the user three Toolkit text input fields. These fields require what is called a backing store. The TKInput objects do not allocate their own memory, instead, they default to having a maximum input length of zero. We will allocate one page and divide it up into three parts: 48 characters for the name field, 16 characters for the age field, and 192 characters for the address field.

After allocating the page, we keep track of that page byte in a variable that is local only to this initialization file. We only need it until the TKInput objects have their backing stores configured. After that, the TKInput's hold the pointers, and main.a will not need to reference this pointer. Setting our zero page pointer, ptr, we can write some zeros as null terminators into the page, to define the boundaries of the 3 TKInput buffers.

Loading Libraries



Next we will load two shared libraries. The first is the path library which is only needed during initialization and will be unloaded at the end of initialization. The second is being loaded as an example of how the TestGround Application is used for testing various C64 OS technologies. We'll load in the memory lib to test and examine how the realloc routine works.

To load a shared library we use the call loadlib from the service KERNAL module. The KERNAL documentation describes in my more detail how this works. The library's 2-character identifier are put into X and Y, the page size plus loading flags are put in the accumulator, and then a JSR to loadlib. Loadlib returns with with the first page of the location of the library in the accumulator.

Loaded or Referenced

It is not known to us if this library was already loaded, and our call to load it merely incremented its reference count, or if our call to load it caused it to be loaded for the first time from disk.

Which it was is not relevant to us. The only thing that is important is that we match each loadlib call with an eventual unldlib call, when we no longer need the library.

Similarly to how we link to the KERNAL, we have to link the routines that we need from a library to the address where the library is found. In order to keep the links to external routines together, it is recommended to put the library jump tables at the end of the code, either before or just after the KERNAL link table. Just as you don't need every KERNAL call, you often don't need every routine provided by a library. In this case, out of 7 routines provided by a path lib, we only need to call 3.

At the end of the code, we'll return to this later, just before the KERNAL Link table, we have the following jump table.

The numbers, 3, 6, and 18 are defined by //os/h/:path.h, and this file could be included and the constants to those numbers could be used. Sometimes, when coding natively, too many includes can slow down Turbo Macro Pro. Each new included has to be loaded and brings in labels that we don't necessarily need. In this case, I've simply used the numbers, and their meanings don't even need to be commented, because the label to the left of the JMP is exactly the name of the routine in the library. You might think, but what if those numbers change? They won't won't change. They're like the offset values of KERNAL routines within a module. If they ever were to change, they would break everything that has ever been assembled against the constants in the past.

Note that a JMP instruction does not a have a zero page addressing mode. Therefore, even though the values used are less than 256, the assembler always converts these into 2 byte addresses. And because the 6502 is little endian, the above table will be assembled like this:

setname	$4C, $03, $00
pathadd	$4C, $06, $00
gopath	$4C, $12, $00

Shared libraries are always page aligned. When the table is first constructed, the page byte is hardcoded to $00. The process of linking this table to the shared library, then, is just to write the page byte returned from loadlib over top of those three $00 bytes. And that is what we see in the code.

	sta setname+2
	sta pathadd+2
	sta gopath+2

Loadlib has returned the page byte in the accumulator, so we write that byte to each of our library jump table entries, plus 2, to write it into the page byte of each JMP instruction.

The second library loaded is memory.lib. We only link a single routine from that library, by writting sta realloc+2. setname, pathadd, and gopath are only used by init.o. And the path lib will be unloaded at the end of initialization. Realloc, on the other hand, will be used throughout the life of the Application, and therefore, the JMP to realloc is found in main.o, just above main.o's KERNAL link table. That is all that it takes to load and link shared libraires.

Loading Config File



Next, we'll look at how to load previously saved configuration data. Our Application is going to save a block of configuration variables to a file in the Application bundle every time it quits. During initialization, we are going to load the contents of that file such that they overlay the existing block of variables in memory.

This is the reason why config.i files (and, by convention, all other files with .i extension) are auto-generating and can safely be scratched. The block of config variables is already in memory with default values. The code then attempts to open config.i. If the file is not found, it skips over the fread and closes the file reference. The block already in memory is thus unmodified, and the Application uses the default values that it was assembled with. Alternatively, if the config.i file is found, its contents are read in and overwrite the default values with whatever values were saved from the last time the Application wrote out that block of memory.

The name of the config file, (typically config.i, but it doesn't have to be,) is used during initialization, but also to save the config data when the Application quits. Therefore, this filename is found in main.a. The block of config variables, which gets used throughout the life of the Application, must also be found in main.a.

System Resources and Bundle Resources

There are times when your Application needs a resource relative to the system directory. In order to find the resource, you start with a base file reference that points to the system directory. This is provided for you, it is called the System file reference. It is always available. It's found in Workspace memory, and is configured by C64 OS during boot up. A KERNAL call, getsfref, in the service KERNAL module can be used to get a copy of the System file reference.

At other times, your Application will need to get a resource relative to its own Application bundle. When an Application is launched, a 1-page file reference is allocated and configured to point to the Application's own bundle. The page number where this file reference is found is stored in workspace memory, with the label appfileref. It is called the Application file reference.

The device number, partition number, and directory path of the Application file reference are configured for you. To access a file in the root of the Application bundle, you only need to set appfileref's filename. Note that we just finished loading the path library and linked to 3 of its routines. One of those routines is setname. We can use that library call to make the task of setting the file reference's filename easier.

Use the macro #ldxy cnffile to get a RegPtr to the string "config.i", load the page number of the appfileref into the accumulator, and call setname. Next, use the #rdxy macro to load a RegPtr to the appfileref. This can be passed to fopen. The flags #ff_r are passed to fopen in the accumulator, to open config.i for read. If the file is not found, the carry is returned set, and we use that to skip over the fread, straight to the fclose. The RegPtr to the file reference is returned from fopen, so it is not necessary to keep reloading X and Y between the file calls.

If the config.i file was found, we fall through to fread. The two .words that follow fread are inline arguments for: where to read the data to, and how much data to read. In this case it only reads 2 bytes and overwrites the block of config variables found in main.o. And then fclose closes the Application file reference.

More detailed explanations of fopen, fread and fclose can be found in Chapter 4: Using the KERNAL → File (module)

Loading the Sample Text Resource



The TestGround Application has a tab that shows a text file. It demonstrates the TKText class's ability to auto-wrap text and do text scrolling with the mouse and built-in keyboard shortcuts. In order to show some text, we have to load a text file into memory. The sample text file is stored in the Application bundle.

The name of this file is only needed once, during initialization, when we load the file in. Therefore, the file's name, at label txtfile_n, is in the content section of init.a. We need to set the filename into the appfileref. Loading this text file resource is very similar to loading the config.i file, but with a couple of important differences. When we loaded config.i it was loading a known fixed number of bytes to a known fixed address. But when we load this text file, we do not know in advance how big it is. You could measure the size of the file ahead of time and assemble those numbers into the program, but that isn't advisable. The point of the text being a separate resource file is so that it can be modified without needing to reassemble the code.

First we use the setname library call to change the filename of the appfileref to the name of our text file, sample.t, and then load a RegPtr to the appfileref. But this time, instead of calling fopen only with the ff_r flag, we OR together ff_r and ff_s. This opens the file for read, but it also performs a stat on the file. This tells fopen to fetch the file's block size and save it into the file reference structure. In this case, we are not checking to confirm that the file exists; we are imposing the requirement that this resource file must exist in the bundle for the proper functioning of this Application.

fopen returns the RegPtr to the appfileref. We need to read data out of the structure, so we write the RegPtr to the temporary zero page pointer, ptr. The block size of a file can be converted to pages. Because a page is slightly larger than a block the file data is guaranteed to fit in the buffer, with some portion of the final page not fully occupied. So we read the low byte of the 16-bit frefblks property from appfileref, to get the approximate page size of the file.

File sizes to memory sizes

The maximum file size that C64 OS supports is based on the maximum 2-byte file size that can be returned by a directory entry.

The file size is in 254-byte blocks. These correspond very close to 256-byte memory pages. If the high byte of the file size is greater than zero, the file would be huge. It would be bigger than the total amount of memory available in the C64. Therefore, unless you know the file is something greater than 64KB, it is usually safe to ignore the high byte of the file size.

high byte = 256 * 254bytes = 65,024 bytes

There are two things we need to do with this page size:

  1. Use it to allocate a block of memory pages, and
  2. Use it to specify how much data to read from the open file reference.

Lower in the code (above,) you can see that the two inline variables following the fread have labels: raddr and rsize. We take the file's block size's low byte and write it to the high byte of rsize. Note that rsize is not in pages, it's in bytes. Thus file block size low-byte is used the high byte of the read size.

Next, that page size is transferred to the X register, and the accumulator is loaded with an allocation type of #mapapp, then we call pgalloc in the memory KERNAL module. We use the allocation type #mapapp to identify that these pages are owned by the Application. This makes it easier to deallocate memory later, and shows the user in the Memory and Usage Utilities how the memory is being used.

pgalloc returns the first page byte in the Y register. We stash this to a temporary variable in init.a, txtbufpg, because it will be used later when we assign the data to the TKText object. We also write this page byte to raddr's high byte. That sets the buffer for where fread should put the data.

Last step. Load a RegPtr to appfileref again and call fread. The buffer pointer and the amount of data have already been configured. Note that, the amount to read is going to be slightly larger than the amount of data in the actual file. fopen reads until either: it hits the size requested in the inline variable, or it reaches the end of the file.


It may seem like this was a lot of work. But we should pause a moment to consider how C64 OS makes some things possible that typically, on the C64, would be very difficult. We found a file at a dynamic location in a complex file system. No reference to a device number, or a partition, or the system directory, or the Application's bundle name was needed, yet we found the file exactly where it was supposed to be. We don't know how big the file is, but with a single #ff_s flag passed to fopen, it gets the file size for us. Without needing to know how much data there is, and in fact, the amount of data could vary between runs of our Application, and without knowing where in memory we can fit that data, we have dynamically allocated enough memory for it. And then we've loaded the data into that memory buffer.

It may seem a bit complicated at first, but in a mere 20 lines of code, we've accomplished something that is so difficult that 99.9% of typical C64 software would never even bother to do it.


Loading and Linking Toolkit Classes



Some simple user interfaces can get away with using just the 9 built-in classes. Especially because the TKButton class can be put into modes for push buttons, cycle buttons, radio buttons and checkboxes. However, if you want your interface to do something more exciting, you'll probably have to load in some custom classes.

In the code above, we load and link 6 classes. The first 5 are all loaded the same way; they are officially supported classes provided by C64 OS, but they aren't permanently memory resident. They are stored in the //os/tk/ directory. The 6th class we'll load in is totally custom to this Application, and the class file is found in the Application bundle.

In the data structures and content of init.a, right at the beginning, we had 4 labels: tkpath, classes, csizes and cnames. tkpath is just the string "tk", to find the toolkit directory. classes is pointer store for 6 pointers. Each will point to the class definitions of the 6 classes we'll load in. The pointer store thus has 6 indexes, 0 to 5. The csizes is a list of 6 bytes, indicating the sizes (in memory pages) of each of the 6 classes. Anda cnames uses the #strxyget macro to help us fetch 6 different class names at each of those indexes.

Loadable Class Sizes

The sizes of the classes are hard coded. If the classes were under development and liable to become a page bigger in the future, then this hard coding would be problematic.

However, in practice, the world will bend over to ensure that these classes remain forever backwards compatible, including not growing in size. Most classes do not occupy the full final page anyway, leaving wiggle room for bug fixes and minor updates. For example, if a class assembles to $0535 bytes, then it requires an allocation of 6 pages. However, the last page will have $100 - $35 = $CB unused bytes. Changes to the class over time can fill up this space without changing its need for a 6-page allocation.

These classes are loaded using two helper routines: loadclass and ldlclass (which stands for load local class.) We will review the details of how these work when we arrive at their implementation near the very end of init.a. However, briefly, the way that a class is loaded is that a block of pages is allocated, sufficiently large to hold the loaded class. Then the first page of that allocation is configured as a file reference to the file to load. The file reference is then passed to the loadreloc KERNAL call.

The loadreloc call automatically calls the first jump table entry in the relocated binary. In the case of a class, this is its link routine. In order for the class to be linked into the class hierarchy properly, a pointer to the superclass of the loaded class must first be installed into the class workspace variable. That's what's going on with each of these. Before loading tklist.r, first a pointer to TKView is looked up using the classptr KERNAL routine. That is because TKView is the superclass of TKList.

Similarly, TKScroll is the superclass of TKTCols. TKList (which we previously loaded and linked to TKView) is the superclass of TKTable. TKCtrl is the superclass of TKInput. TKView is the superclass of TKText. And finally, TKView is also the superclass of our totally custom class TKFooter. If in doubt about what the superclass of a given class is, this can be discovered by looking at the headers in //os/tk/s/ or //os/tk/h/. For example, in tktext.s, the first property is at tkviewsz. And in tktext.h the first method offset is at tcviewsz. Both of these are because TKText inherits from TKView.

The class hierarchy can also be determined by looking it up in the Appendix XI. Toolkit Classes of the online version of the C64 OS User's Guide, or in the Appendix IV. Toolkit Classes of the Programmer's Guide.

Building a Toolkit User Interface

Now that the extra Toolkit classes have been loaded and linked into the Toolkit class hierarchy, it's time to start instantiating them, configuring them, and linking them together in a node hierarchy to build the User Interface of our Application.

Before you can start instantiating Toolkit classes, you have to set the Toolkit environment. Part of the environment is a pointer to a memory pool, out of which all internal calls to malloc will be allocated. It's okay to make the memory pool bigger than it needs to be, and this might even be what you want, allowing you to later make your own abitrary allocations with malloc from the same pool. Making the memory pool too small, though, is definitely a problem. As you instantiate new classes, if you run out of memory and you aren't explicitly trapping and handling exceptions, your Application will crash. The KERNAL call tknew in the Toolkit module raises an exception if malloc fails to make the required allocation.

The best way to know how big of a memory pool your user interface needs, is to draw the interface out and consider which classes you can put to use, and how they can be nested together to accomplish the layout you want. I typically draw out my user interfaces on graph or grid paper. A full screen of 40 columns by 25 rows neatly fits on a sheet of graph paper in landscape orientation, with room around the outside to make notes about how the Application or its user interface should work.

The following is an example diagram used for the TestGround App. There are additional markings and layout guides that will be discussed below.

Paper Mockup of TestGround UI.
Paper Mockup of TestGround UI.

This paper mockup eventually leads to the actually Application's UI which looks like the following screenshots:

TestGround UI - Table Tab in Focus. TestGround UI - List Tab in Focus TestGround UI - Text Tab in Focus

Once I have an idea about how the interface will be laid out, I write out the names of all the classes that need to be instantiated, in a nested bullet point list. In a second column, I write the size in bytes of each object. Sum up the size of all the objects in the UI, and at the bottom write the total.

The next consideration is that every allocation made with malloc includes a 3-byte header. For example, if the interface consists of 25 objects, add 25 * 3 = 75 bytes to the total requirement. Round this number up to the nearest increment of 256, to get the smallest number of pages absolutely required to hold all of the Toolkit objects. From there, depending on the need of the Application, more pages can be added for extra space for making runtime allocations. In the TestGround app, we will not require any additional space, so the minimum number of pages needed for the UI is what will be allocated.

Here is what the node hiearchy looks like, and its minimum size requirements calculation:

Pointer Store Class Size Notes
Root TKSplit 45  
views[0] TKScroll 46 Left Split Content
TKSBar 65
views[1] TKView 39
tstviews[0] TKLabel 44  
tstviews[1] TKLabel 44  
tstviews[2] TKButton 62 Radio 1/2
tstviews[3] TKButton 62 Radio 2/2
tstviews[4] TKButton 62 Radio 1/3
tstviews[5] TKButton 62 Radio 2/3
tstviews[6] TKButton 62 Radio 3/3
tstviews[7] TKButton 62 Checkbox 1
tstviews[8] TKButton 62 Checkbox 2
tstviews[9] TKButton 62 Checkbox Show Footer
tstviews[10] TKButton 62  
tstviews[11] TKButton 62 Cycle Button
tstviews[12] TKButton 62 Go Button
tstviews[13] TKButton 62 Find Button
tstviews[14] TKLabel 44  
tstviews[15] TKLabel 44 Name
tstviews[16] TKLabel 44 Age
tstviews[17] TKLabel 44 Address
tstviews[18] TKInput 60 Name
tstviews[19] TKInput 60 Age
tstviews[20] TKInput 60 Address
views[2] TKView 39 Right Split Content
views[3] TKTabs 54  
tabviews[0] TKTCols 54 Table Tab Content
tabviews[1] TKTable 51  
TKSbar 65  
TKSbar 65  
tabviews[2] TKScroll 46 List Tab Content
tabviews[3] TKList 49  
TKSbar 65  
tabviews[4] TKScroll 46 Text Tab Content
tabviews[5] TKText 52  
TKSbar 65  
TKSbar 65  
views[4] TKFooter 39  
  Object Total 2138  
  39 Objects * 3 117  
  Total Required 2255  

2255 divided by 256 = 8.8 pages. Round that up to the nearest page to conclude that this user interface requires a minimum of 9 pages. In the code below, then, we are allocating 10 pages. It would probably work with 9 pages, but if you make a mistake in your calculations it's better to be a bit bigger than necessary. Later when the Application is running you can test it empirically to see if the 10th page is truly not being used, and then tighten it up later.

The allocation is made again with type mapapp because this memory is owned by the Application. The first page number of the block is returned in the Y register, which we write into the Toolkit environment structure at offset te_mpool. This is necessary to set up the memory pool that this Toolkit environment will use. The tkenv structure is used throughout the life of the Application, so it is found in main.a.

Now that the memory pool is set up, before we can start instantiating Toolkit classes we must set our tkenv structure as the current Toolkit environment. Use #ldxy macro to load a RegPtr to tkenv, and call settkenv in the Toolkit module.

Instantiate the Root View

Every Toolkit user interface must have a single root view, that can be a TKView or any subclass of TKView. Its anchors and offsets are relative to the bounds of the whole draw context. In TestGround, the root view is a TKSplit configured with a vertical splitter.

Instantiating any Toolkit class generally takes the same form:

  • Get a pointer to the class and call tknew.
  • Save the pointer to the new object for later reference.
  • Call the init method on the new object.
  • Configure the object by overwriting its default properties.
  • Append the object to its parent object in the node tree.

How you get the pointer to the class depends on what class it is. TKSplit is a built-in class, so you can get a pointer to it using the classptr routine in the Toolkit module. The ID of the class is passed in the X register. The class IDs are defined by //os/tk/h/:classes.h

classptr returns a RegPtr to the class, and tknew takes a RegPtr, so tknew can be called immediately following the call to classptr.

The root view is a little bit special. It doesn't have to be appended to its parent, because it has no parent. Instead, a pointer to the root object must be set into the toolkit environment. This serves the purpose of saving a pointer to the object, and also establishing it as the root view of the view hierarchy.

Calling an object's method is done in two stages: preparing to call the method with getmethod, and then actually jumping to that method with sysjmp. Between those two stages the parameters to pass to the method can be loaded into the registers. To prepare an object's method, first the object must be the current this object. This and class (the class of this) are set automatically by tknew. Load the method index into the Y register then call getmethod in the Toolkit module. You can then load the parameters to pass to the method into the various registers, and finally execute the method with a JSR to sysjmp; sysjmp is found in zero page and is defined by //os/s/:service.s.

After calling the init method on an object, all of its properties have been initialized to valid default values. To configure the object, it is only necessary to change the properties whose defaults are not as you want them to be. The Setter and Getter macros discussed in Chapter 3: Development Environment → Common C64 OS Macros are ideal for getting and setting properties on Toolkit objects.

The TKSplit class inherits from TKView, so it has all of the node properties that TKView has, plus it has several properties specific to being a splitter that TKView does not have. We will change the orientation of the split to vertical. We will collapse the splitter bar in order to save space, because there is already a vertical scroll bar that makes for a nice visual divider between the two halves of the split. We will set the background color of the splitter's nub to black. And then we'll set the default position of the split to column 13.

Setting these properties can be seen in the code above, with the group of four #setobj8 macros. this is the object, the second parameter is the offset into the object, and the third parameter is the value to set.

For the root view, last but not least are its anchoring offsets. By default a TKSplit, like a TKView, is anchored to the TOP, LEFT, RIGHT and BOTTOM of its parent container, and its default offsets from those 4 edges are all zero. This is usually what you want. The root view's parent container is the full draw context. For an Application the draw context typically fills the whole screen. The problem is that the menu bar and the status bar are drawn on a higher screen layer in the first and last rows of the screen.

By default, the root view would stretch all the way from the top to the bottom of the screen and the menu and status bars would be drawn partially covering the top and bottom of the root view. To accommodate this, the offtop and offbot properties are each set to 1. This pulls the top down one row from where it's anchored, and it pulls the bottom up one row from where it's anchored.

Later, we will see how to respond to a message that informs us of when the system's redraw flags have been changed. This message arrives if the user shows or hides either the status bar or menu bar. In response to that message, these offset properties can be adjusted in realtime.

Instantiate the left split content

Each side of a TKSplit object must have a single container view. In TestGround, the left side contains many small controls: Labels, radio buttons, checkboxes, a cycle button, push buttons, and text fields. These all have to be organized within a single container. What's more is that they do not fit vertically within the height of the screen. We want the full column—the stack of smaller controls—to be able to scroll up and down.

To accomplish this, the container view for the left side of the TKSplit will be a TKScroll.

Get a pointer to the tkscroll class and call tknew. Next, save the pointer to the new object.

When a user interface is small and simple it is often enough to have just a single pointer store. But TestGround has a fairly complex UI, so I've divided the interface into 3 main sections. The first, simply called views, are the root view (TKSplit) plus the 5 backbone views: The left scroll view, and its content view, the right content view, the TKTabs view, and the custom TKFooter view.

The second main group is called tstviews (meaning Test Views), which are the stack of labels, buttons, checkboxes, and fields that run down the left side. And the third main group consists of the six views within the 3 tabs: The table columns view, and its table view, a TKScroll view, and its TKList content, another TKScroll view, and its TKText content, these six are held in the pointer store called tabviews.

In the table earlier, showing the hierarchy of all the objects in the TestGround user interface, the first column is labeled "Pointer Store." This shows the name of the pointer store and the index where the pointer to each object is saved. The macro #storeset views,0 above shows how this TKScroll object is saved to the views pointer store at index 0.

Next we call the init method on the TKScroll object. And the only property that we override is something called "inset." This is changed from 0 (default) to 1. Previously, we saw that the vertical splitter bar was collapsed. This means that the splitter bar doesn't take up any space. That means the the way the TKScroll manages the sizes and positions of its left and right sides such that they abut each other. This looks good and works well in this context because the right side of the left hand content view is a long vertical scroll bar. It serves as a natural divider between the two sides.

The only problem is, that scroll bar would ordinarly run all the way from top to bottom, and cover over any area that you could grip to resize the splitter. By setting the TKScroll's inset property to 1, it shortens the vertical scroll bar by one row. This leaves a single character just below the scroll bar where the TKSplit draws its resize handle.

Diagram showing the effect of collapsing the splitter and insetting the scrollbar.
Diagram showing the effect of collapsing the splitter and insetting the scrollbar.

Collapsing the split and insetting the scroll bar is a design choice that you have to make. It saves screen real estate, I think it looks nicer, but it also shrinks the available area where the user has to click and resize the split. When the splitter is not collapsed, the split bar runs vertically all the way to the top, and the user can click anywhere on that bar to drag and resize it, just as you can see in File Manager.

The last thing to do is append the TKScroll view to the TKSplit view. To do this, we use the appendto routine in the Toolkit module. It starts with this being the view to be appended and takes a RegPtr to the view being appended to. Using the macro #rdxy tkenv+te_rview, we get a RegPtr to the root view. You can append exactly two times to a TKSplit view. If you attempt to append a third time, it will raise an exception. The first view appended becomes either the left or the top half of the split. The second view appended becomes either the right or the bottom half of the split.

Instantiate the left scroll content

Just as TKSplit manages the layout of two content views, the TKScroll can also only manage a single scrollable content view. We want our left side scrolling column to contain many small controls, though. To accomplish this, we will instantiate a single TKView as the TKScroll's content view. And TKView supports the arbitrary layout of many child views.

Get a pointer to TKView using classptr, then call tknew. Save the pointer to the new object to the pointer store views at index 1. Next, call the init method on the TKView object; it takes no parameters.

TKView has a draw flags property (dflags) which all subclasses of TKView inherit. One of the draw flags is whether the view should be opaque or transparent. By default, the opaque bit is disabled. When a transparent view draws itself, it only fills in the characters that are explicitly overdrawn, leaving any areas not explicity overdrawn showing whatever content was originally there beneath it.

There are two main reasons for views to be transparent. The first is by design, so that one view can overlay another and add small flourishes to it but without completely obscuring it. The second reason is because the natural drawing routine of the view already overdraws every character in its draw context. Setting the opaque flag causes the TKView's implementation of the draw routine to first clear the draw context, then the subclass's draw routine can draw sparsely into the cleared context. If the subclass's draw routine already overdraws every character in its draw context then there is no need to use the opaque flag, and setting it would make drawing slower and less efficient.

However, in some contexts, this being an example, we need the TKView content view to be opaque so that the areas between the layout of its child views don't get ghosted in the background as the content view is scrolled up and down. We use one of the convenient flag macros, #setflag this,dflags,df_opaqu to set the df_opaqu flag in the dflags property byte on this.

Next, we will append the content view to the TKScroll view. TKScroll is a bit special in this regard. The reason is because it automatically manages the anchoring and offsets of its content view, and it automatically instantiates, appends to itself, and wires up callbacks between itself and one or two TKSBar objects. Therefore, we don't use the standard appendto routine. Instead, the TKScroll class has a setctnt method (meaning Set Content).

In order to call a method on the TKScroll object, it has to be made the this object. We recall our saved pointer from the views pointer store at index 0 and call the routine ptrthis in the Toolkit module. Now the this context is the TKScroll object and class is also updated to the class of this.

Now we can prepare to call setctnt by using the getmethod routine. And before executing the method with a JSR sysjmp, we first load a RegPtr to the TKView content view by using the #storeget macro to pull a pointer from views at index 1.

After setting our TKView object as the main content view of the TKScroll, the TKScroll is still set as the this object. We can now enable one or both of the TKScroll's scrollbars. There is no need to manually create or append a TKSbar. The TKScroll has a method for managing its scrollbars, and this can be called at runtime to turn on or off either scrollbar. Prepare to call the setbar method, using getmethod. The state on/off state of the vertical scrollbar is loaded into the Y register, 1 for on, 0 for off, and the on/off state of the horizontal scrollbar is loaded into the X register. In this context, we'll turn the vertical bar on and the horizontal bar off. Execute the method with JSR sysjmp, and it's done.

Instantiate the right content view

We'll continue with the backbone views, by creating and appending a content view to the right side of the TKSplit (root) view. The right side doesn't scroll in its entirety, so we don't need a TKScroll here.

You should be starting to see the pattern of how these are put together, now. Get a pointer to the TKView class, call tknew, save the pointer to views pointer store at index 2. Prepare to call the init method and then call it. It takes no parameters.

According to the design of this App, we'll put a TKTabs view and the custom TKFooter view into this right side content view. But those will be anchored inset leaving a nice 1 character border on all sides. This is done purely for style, I think it looks nice. The border left around the children necessitates that we set the opaque draw flag on the container view. Otherwise, as the TKSplit gets resized, and this content view gets resized, ghosted bits of its child views would appear in the borders.

Indicating the insets of TKTabs and TKFooter.

In the image above, the red arrows indicate the top, left, right and bottom insets of the TKTabs view from its parent container. The three green arrows indicate the left, right and bottom insets of the TKFooter view from its parent. The TKFooter has a fixed height, and is anchored left, right and bottom only.

Additionally, because we have this opaque border, we need to choose a color. This gives an opportunity for user customizability, automatic settings, and also the use of the Colors Utility. Here, we only need to read a color value from a block of config variables and assign that color as the starting background color (#bcolor) of the TKView. The config variables are found in main.a. To complete the customization, we will see later how to listen for color selection messages, and how to update the background color of this view in response.

Lastly, we append this TKView to the TKSplit. That's the root view, so the pointer to it can be pulled from the Toolkit environment structure, and then passed in a RegPtr to the appendto routine.

Instantiate the TKTabs view

Continuing along with the backbone views, we'll now instantiate the TKTabs view, configure its anchoring and offsets discussed above, give it a delegate structure, and append it to the right content view.

TKTabs is another built-in class, so we can get a pointer to it with classptr, and then instantiate with tknew. Save the pointer to the new object with #storeset to views at index 3. Again, call the init method on the new object, it takes no parameters.

A TKTabs class inherits from TKView. A TKTabs object derives its anchoring properties from the TKView superclass, which are: top, left, right and bottom by default, with insets of zero for all four sides. The only thing we have to do is override the offsets. We'll set the offtop (offset top) to 1, and the offbot (offset bottom) to 6. That offset bottom 6 is to make room for the fixed-height TKFooter. Both of these properties are set using the #setobj8 macro.

Now, we also want to set the left and right offsets to 1 each. However, these are a very common adjustment that will be applied to many objects in our user interface. The macros get expanded like this:

#setobj8 this,offleft,1
#setobj8 this,offrght,1

Becomes...

ldy #offleft
lda #1
sta (this),y

ldy #offrght
lda #1
sta (this),y

The macros are convenient, but it takes 12 bytes to set those two values. To shorten the code, this common pattern has been put in a subroutine called lmargin, found later in init.a. Now, everywhere where we want to set these left and right offsets of 1, it takes only 3 bytes to call: jsr lmargin. The nice thing about object-oriented code is that the "this" pointer in the lmargin routine is always whatever is currently the this object.

Delegation

Next, we have a new concept; delegation. In more flexible languages where method and property names can be looked up dynamically, this pattern allows an object to be configured as another object's delegate. Often, when an object is first instantiated its delegate pointer points to itself, and it itself provides simple but valid behavior for each of the delegated tasks. The programmer can later (even at runtime) configure the object to point at a different delegate. Or multiple objects can even share the same delegate.

In 6502, where we are extremely memory constrained—and we don't have many cycles to spare either—the object-orientation implementation is less flexible. Methods and properties have fixed numeric offsets once assembled. This means it isn't possible to use any arbitrary object as the delegate for some other, because the numeric offsets of the methods wouldn't line up. Instead, the Toolkit in C64 OS retains some of the behavior of delegation but in a simpler way that is better suited to the Commodore 64's limitations.

A delegate is essentially a set of callbacks that are collected together in a structure. Delegation allows an object to reach out to some external controller to get realtime answers to some of its behavior, without the external controller being required to constantly update state variables in all of its objects. The pattern is a form of inversion of control and can lead to much less code and much cleaner code.

When a TKTabs object is initialized, its delegate pointer gets set to a static implementation provided by the class. The TKTabs delegate is a structure of pointers to 5 routines. The delegate routines are defined by //os/tk/s/:tktabs.s. They are:

  • tabstr
  • willblur
  • willfoc
  • didblur
  • didfoc

The first, tabstr, is to get a pointer to a string that is to be the title of a tab. Every time a TKTabs object is redrawn, (i.e., has its draw method called,) it makes one call this routine for each tab. When the routine is called, this is pointed to the particular TKTabs object that is being redrawn, and the tab index (starting at 0) is passed in the Accumulator. The routine returns a RegPtr to the string to be used for that tab's title.

In other words, the tab titles are not stored by the TKTabs object. Nor are pointers to the tab title strings stored by the TKTabs object. The pointers are dynamic and the strings are stored—and can thus be easily manipulated—by whoever owns the delegate. By default, the delegate provided by the class returns the tab titles as "Tab 1", "Tab 2", "Tab 3", etc. (i.e., the tab's index +1.) To change the tab titles, you must provide a custom delegate, and implement the tabstr routine. Because the delegate must persist throughout the lifetime of the Application, it is implemented in main.a.

The other delegates routines are called in sequence around the changing of tabs. They allow the controlling code be notified that a given tab will lose focus (blur) and that a give tab will gain focus. Each of the "will" delegate routines allow the controller to approve or deny the change, as well as to take other actions. When willblur is called, the current focus tab can be looked up on this. To deny the tab the ability to lose focus, return with the carry set. To allow the tab to lose focus, return with the carry clear. Only if the tab is allowed to lose focus will the code proceed to the next step. It then calls willfoc and passes the new tab index in the Accumulator. Again, to deny the new tab taking focus, return with carry set. To allow it to take focus, return with carry clear.

The two "did" delegate routines are called to inform the controller that one tab has already been blurred, and that another tab has already been focused. These routines cannot affect anything by what they return, these are just notifications that the change has taken place. The default delegate of TKTabs is configured with clc_rts, clc_rts, raw_rts, raw_rts for the 2 wills and the 2 dids. That means, both wills are allowed and both dids simply return immediately without doing any other work.

Setting the delegate

Setting the delegate pointer on a TKTabs object is done with the setdel method. Prepare the method to be called. Load a RegPtr to the delegate structure. And jsr sysjmp to execute the method.

Lastly, we will append the TKTabs view to its parent view. In this case, we're appending it to the right content view. Get a pointer to that view with #storeget views,2 and call appendto.



Instantiate the TKFooter view

Continuing now to our last backbone view, we'll instantiate the custom class, TKFooter, configure its anchoring and offsets and append it to the right content view just after the TKTabs.

TKFooter is the first class that we are instantiating that is not one of the built-in classes. Rather than using a class ID and calling classptr, we use the #storeget classes,5 macro to fetch a pointer to one of the classes that was loaded earlier in init.a.

After creating the new object with tknew, store it to views at index 4. The call its init method, with no parameters. And lastly, append it to the right content view by getting a pointer to the right content view with #storeget views,2 and calling appendto.

You may have noticed that we didn't actually have to configure any of its anchoring or offsets. Why is that? The reason is because TKFooter is totally custom class. It is implemented just for this Application only. The class itself is stored in the Application's bundle, so other Applications shouldn't even have access to it.

TKFooter's own init method configures its anchoring and offset properties to the defaults that are sensible for where it will plug into this UI. If you wanted to move it, for example if you wanted to put the TKFooter above the tabs, you could still do this without reassembling the TKFooter class, by simply overriding these defaults after calling init. But having the defaults in its init method means you don't need to set them here explicitly.

API Guideline: Honor the privacy of other Applications' bundles

Although it is possible for your code to configure a file reference that points to the contents of a different Application's bundle, you should never do this. It is only possible because the Commodore 64 does not have the ability to enforce a restriction preventing it.

However, the internal contents of an Application's bundle are intended to be private. If an Application wants to share some resources, such as a library or a Toolkit class, the user should put copies of those items into the //os/library/ or //os/tk/ directories, or your Application should get stable copies of those resources to store in its own bundle.

If your Application reaches into the bundle of another Application expecting to find a resource, it is liable to break in one of several ways:

  • The user could uninstall the other Application altogether.
  • An update to the other Application could change the name of the resource.
  • An update to the other Application could change the API of the resource, correctly expecting that it should be able to do this without breaking an external dependency.

Creating the Left-Side Test Views

The backbone of view structure of the Application is now in place. We've populated all 5 slots of the views pointer store. It's now time to create and wire up all the sample and test views that will appear in the left-hand scrollable column. All of the pointers to these objects will be stored in the tstviews pointer store.

Instantiating the main title

The main title is a TKLabel, with centered text that uses the strong text color from the Theme.

TKLabel is a built in class, so fetch a pointer to the class with classptr and call tknew to instantiate. Store the pointer to tstviews,0. Call the init method with no parameters.

By default, a TKLabel is anchored top, left and right, with a height of one, and the text is left aligned. //os/tk/s/:tklabel.s defines the strflgs (string flags) bits for alignment: a_lft for left, a_rgt for right, or you can or those together for center alignment. The only problem here is that using a_lft.a_rgt overflows the 40 column line length in TMP, so I've used the bit values manually.

Next we'll set the color of the text. TKView already provides the bcolor property, which TKLabel (which inherets from TKView) uses for its text color. bcolor stands for background color, and it would be the label's background color if TKView's reverse draw flag were also set. To get the bold or strong text color, we read that from tkcolors+c_stgtxt, the global theme color. These are defined by //os/s/:ctxdraw.s and //os/s/:ctxcolors.s. We'll see later how to update this in response to a message about the theme being changed.

We need to get a pointer to the string to set as the content for this label. The method to set the label's string content is setstrp, so we prepare that method first. Then we fetch a string pointer from the string pointer store by loading the string index 0 into the accumulator and calling uitest_s. uitest_s is implemented in main.a, because the Application needs to have access to these strings throughout its life. We'll return to talk about string stores later, but what's important now is that the macro used to create this string store returns the string pointers as standard RegPtrs (i.e., in X and Y). However, for technical reasons related to the internal implementation of TKLabel, it requires the pointer to its string be passed in A/X (low byte/high byte.) There is another convenience routine implemented in main.a called xytoax which when called converts a RegPtr to an AX pointer. And then we can call the method with jsr sysjmp.

TKLabel is already anchored top, but its defaut offtop property is set to zero. This is its relative offset from the top of its parent view. Since we're going to be adding many views, more or less in a column, to the same parent view, each needs to have its offtop set to lay them out within that parent, one after the next. We'll set this one's offtop to 1, which means there will be a single blank row above this label.

It's already anchored left and right by default, but by calling lmargin, we set the left and right offsets each to 1, as seen earlier when discussing TKTabs. The only thing left to do is append the label to its parent. Same as always, use #storeget views,1 to get a pointer to the left content view, and call appendto to append this to it.

Instantiating the subtitle

Following the centered bold main title, is a standard color, left-aligned section title called options, that will introduce a series of radio buttons and checkboxes.

This is implemented in precisely the same way as the main title. In your code you will surely copy and paste, and then modify to customize the specifics.

Instantiate a new TKLabel, store the pointer at tstviews,1. Then call the init method. No need to change the color or the alignment this time, because left aligned and standard text color are the defaults for TKLabel. Prep the setstrp method, this time we'll get a pointer to string #1 with uitest_s, convert the pointer format to AX and call the method.

Again, the offset top needs to be set to position this label. We'll set it to 3, so there is a one line gap between the main title and this subtitle. Call lmargin to set the left and right offsets. Append to the parent view. Wham bam.

Instantiating the first set of radio buttons

Next is a new Toolkit class that we haven't used yet. We'll create a pair of radio buttons and then we'll link them together to form a radio button group. Radio buttons, like checkboxes, cycle buttons and push buttons, are all instantiations of TKButton with custom settings. TKButton inherits from TKCtrl which provides it with additional methods and properties.

We're creating two radio buttons in a row, so their code will be virtually identical. TKButton is a built-in class, so we get a pointer to it with classptr and then tknew to instantiate. The pointer to the first button is saved at tstviews,2 the second radio button is saved at tstviews,3. Call the init method and by default it gets initialized as a standard push button. We'll use the #setobj8 macro to set the button type (btype) property to bt_rad (button type radio.) The propery and its values are defined in //os/tk/s/:tkbutton.s

Next we must set the title string of the button. This is done with the settitle method, and unlike tklabel, tkbutton takes its string as a RegPtr. Prep the settitle method, get the string pointer using uitest_s. The first radio button's string index is 2, the second is string index 3. And call the method with jsr sysjmp.

TKLabel anchors top, left and right by default. But TKButton anchors top and left by default. In this context we want them to be anchored right as well, for both we'll use the #setobj8 macro to set the rsmask property to anchor top, left, and right. "rsmask" stands for resize mask, which is what holds the anchoring flags of a view plus some other bits. It is a property defined by TKView, from which TKButton descends. (TKObj → TKView → TKCtrl → TKButton.) The bit flags for resizing are defined by //os/s/:toolkit.s. This is slightly unusual for properties; usually these would be defined by //os/tk/s/:tkview.s, but these are very low-level and used intimately by routines implemented in the toolkit KERNAL module, hence they are available for general use by any and all classes just by including the toolkit.s header.

As was seen earlier, there is a hard limit of 40 character lines in Turbo Macro Pro (native on the C64.) There is not enough room for:

         #setobj8 this,rsmask,rm_ankt.rm_ankl.rm_ankr

Instead there is just enough room to make the call and express the bits using a binary constant. In my mind, I just remember that the order of bits goes Top, Bottom, Left, Right. So it's easy to remember that 00001101 (read from right to left) is Top = 1, Bottom = 0, Left = 1, Right = 1. I also state in the comment that this line is setting anchoring to TOP|LEFT|RIGHT, just to be clear.

         ;Anchoring T|L|R
         #setobj8 this,rsmask,%00001101

As with every view that is a child of this content view, we have to set its top offset. These radio buttons are set to row 5 and 6 respectively. And they each have their left and right offsets set to 1 via the call to lmargin. Each is then appended to the left container in the standard way.

Linking radio buttons into a radio button group

We have just created two radio buttons. However, as independent objects, they have no awareness that they belong to a group. The eponymous behavior of a radio button is that when one is pushed in (checked on), all the others in its group get pushed out (dechecked). It is necessary now to link these two buttons together to form a group.

The radio buttons in a group are linked in a singlely-linked-loop using the bnext (button next) property. Button 2 will point to button 1, and then button 1 will point to button 2.

Since button 2 just created last, it is still configured as the this object. Thus, we'll begin by setting its 16-bit bnext propery to point to button 1. The pointer to button 1 is in the tstviews pointer store at index 2. Typically we use a macro to get a RegPtr out of the store at an index, and the macro disguises the mathy work that's done to find the offsets. We can't do that here, so the nature of the pointer store is exposed to us. But, they're very simple. A pointer store is nothing more than a block of memory in which pointers are stored one after the next: low/high, low/high, low/high, etc. Therefore, the low byte of a pointer at a given index is simply the index times 2, and the high byte of that pointer is at index times 2, plus 1.

Load the Y index register with #bnext, that's the low byte index of this for bnext. Load the accumulator with the low byte from the pointer store at index 2:

lda tstviews+(2*2)+0

And write that to this. Increment the Y register so it points to the high byte index of bnext. Load the accumulator with the high byte from the pointer store at index 2:

lda tstviews+(2*2)+1

And write that to this. Notice how in my code, I have that extra little +0 on the end? This is totally unnecessary, it doesn't change the value at all. But I like to use it when two references are going to be made in close succession so they look the same length, and my eye is reminded that this is getting the 0th byte of a pair.

I like how this looks:

lda tstviews+(2*2)+0
... 
lda tstviews+(2*2)+1


More than how this looks, even though they mean the same:

lda tstviews+(2*2)
... 
lda tstviews+(2*2)+1

That links button 2 to button 1. Now we have to do the reverse. Get a RegPtr to button 2 using the storeget macro. At this point, we have two options: You could call jsr ptrthis, which is the correct and official way to change the this context (because it changes the class pointer too.) Or, you could just write the RegPtr to the this pointer. I've opted to do that here, knowing that both buttons share a class anyway.

Then we perform basically the same code to link this.bnext to button 2, using the correct index 3 to get the button 2 pointer from the tstviews pointer store.

	
lda tstviews+(3*2)+0
... 
lda tstviews+(3*2)+1

Initiating the set of three radio buttons

As a test to show that radio buttons can be grouped together with varying numbers of buttons within the group, we next create a set of three radio buttons, and then link them in a loop:

Button 3 → Button 1 → Button 2 → Button 3

I will not describe the above code one line at a time. But I've posted it here for you to review and compare to the code that was used to create the first set of two radio buttons. The code is identical, except that the button pointers are stored tstviews at indexes 4, 5 and 6. The string indexes for the buttons are, correspondingly, 4, 5 and 6. And the offsets from the top of the content view are 8, 9 and 10.

Setting the first radio button in this group to offtop 8, when the last radio button from the previous group was offtop 6, leaves a one row gap to visually separate the two radio button groups.

Linking three radio buttons into a radio button group

Once again, this code is virtually identical to the code used to link two buttons together, but it will link three buttons together.

The buttons are linked in a loop. Since Button 3 was the last one instantiated, we'll link button 3 to button 1. Then change the this context to button 2 and link it to button 3, then change the this context to button 1 and link it to button 2.

The two groups of radio buttons are now configured, and we will see in the Application that when we click on any of them it will get checked while all the others in its group get unchecked. However, this is just for show. None of the radio buttons is actually connected to any code; they have not been assigned values or actions.

Instantiating checkbox #1

We'll now move on to instantiate 3 checkboxes. Checkboxes are also instances of TKButton, but their button type property is set to checkbox. Additionally, checkboxes are independent and toggling their state has no effect on other checkboxes. It is therefore not necessary to set their bnext property. If you were to set their bnext property, it wouldn't get used for anything.

This first checkbox is also just for show, it is not assigned any value nor an action.

Just looking at this code, it should be clear that creating a Checkbox is nearly identical to creating a radio button. The only difference is that its btype property is set to bt_chk, and it doesn't need to be linked with bnext to any other checkbox. This checkbox has its offtop set to 12, leaving one blank line following the last radio button which was at offtop 10.

Instantiating the Toggle Wrap checkbox

We'll create one more checkbox, but this time we'll assign a target and give it a default checked state.

Compared to the previous checkbox, this code is almost the same. After setting the btype to bt_chk, we want the state bit of this checkbox to be set. TKCtrl provides a control flags (cflags) property. One bit is cf_state, if it's 1 the control is in a checked or selected state, if it's 0 the control is dechecked or deselected. This is used to indicate the checked state of radio buttons and checkboxes.

Here we use the macro #setflag, rather than the macro #setobj8. #setobj8 would replace the entire property with the value provided, but #setflag only sets the bits on the property that are set in the value provided. #setflags this,cflags,cf_state will only flip on the cf_state bit of cflags, leaving all the other flags as they were.

The only other difference with this checkbox is that it has a target. TKCtrl has a method for setting a target and properties for holding a reference to the target. The action is automatically taken on the target when (a non-disabled) TKCtrl is clicked. This mechanism works exactly the same for all modes of TKButton (radio, checkbox, push button or cycle button.)

There are two ways that for TKCtrl to target. The first is a RegPtr pointer to a TKObject plus the method index in the accumulator. When the TKCtrl is triggered, it performs the action by changing the this context to the target object, prepares to call its method by the provided method index, but then it loads a RegPtr to itself (to the originating TKCtrl object), then it calls the method. When this mechanism is used one Toolkit object is linked directly to another and no business logic is required at all for them to interact with one another. This is the mechanism used, for example, to connect a TKSBar to its parent TKScroll view.

The second way is to provide a RegPtr pointer to a routine that you'll implement in your main business logic, and pass a 0 in the accumulator to indicate that the RegPtr is to a routine, not a Toolkit object. When the TKCtrl is triggered, it remains the this context, and it calls the routine with no parameters. The routine can look up properties on the this object to determine who has originated the call. This allows the same routine to take different actions depending on which TKCtrl called it how that TKCtrl is configured.

In this case, we prepare the method settgt, then load 0 into the accumulator to indicate that we're assigning a routine not an object. Then we load a RegPtr to the togwrap (toggle wrap) routine which is implemented in main.a. And jsr sysjmp to call the method and assign the target.

Instantiating the Show Footer checkbox

We'll make one more checkbox, the code is essentially identical to the code for the previous checkbox, but we will assign this one a different target routine; togfoot, to toggle the footer view on and off.

No comment on how this is implemented should be necessary. It is the same as the previous checkbox. It's offset top is one row lower than the previous one, and its target routine is togfoot. Both togwrap and togfoot are implemented in main.a, and we'll see how those are implemented later.

Instantiating the buttons subtitle

We have already seen above how the options subtitle was created. This is exactly the same, but it has a different string index and a different offtop to position it below the last checkbox.

Instantiating a cycle button

We're already seen how to instantiate a TKButton, but this time it will be configured as a cycle button. There are a few interesting properties and features to explore here to show you how cycle buttons work.

This is, again, largely the same as how any TKButton is instantiated, intialized, offset, and appended. After initialization, its button type (btype) is set to bt_cyc. The button type affects how a TKButton appears and also has some effect on its behavior. A cycle button is drawn like a push button but with a cycle icon to the left of the title text.

This cycle button will retain its default anchoring of top and left. We will not use the lmargin routine, since that sets its right offset, which is only appropriate if the TKView is anchored left and right. Instead, we'll set its left offset to 1 manually, and we'll give it a manual width of 11. This width is based on the amount of space needed to show its longest string plus the space on the left for the cycle button, and a single space on the right.

This example cycle button is being configured to cycle through 12 months of the year. The TKCtrl provides properties to whole a variety of different value types. A TKCtrl can hold a single byte, a 2-byte word, a 5-byte float (in the format used by the BASIC ROM's floating point math routines) or a 2-byte string pointer. TKCtrl has corresponding methods, setbyt, setwrd, setflt, and setstr, which set the TKCtrl object's value and also set the value's type in a valtype property.

To cycle through months of the year, we only need to hold a single byte value. We'll arbitrarily define January as 0 and December as 11. To set the cycle button's default value to January, we prepare the setbyt method, load 0 into the accumulator and then call the method.

Cycle buttons have two behaviors when they are triggered. The first auto-increments the byte value (remember that a cycle button is a TKButton which is a subclass of TKCtrl, so a cycle button may act on its own byte value.) TKButton adds properties that TKCtrl does not have on its own, it adds a minval and a maxval. When the cycle button increments its byte value, if it exceeds its maxval, it automatically wraps the byte value back to the minval. Then, if a target has been set, the cycle button also performs the action either by calling a method on the target object or by calling the target routine.

One additional note. Cycle buttons have the built-in feature that if the COMMODORE key is held down when the button is triggered, it cycles in reverse. Instead of incrementing the byte value it decrements the byte value. And if the byte value falls below the minval, it automatically wraps it to the maxval. This behavior is automatic and requires no additional programming. When you use a cycle button the user may always hold COMMODORE to cycle in reverse.

We use the #setobj8 macro to set the minval to 0 and the maxval to 11. This automatically constrains the cycle button to the valid range of month values.

The target of the TKButton is set to the routine mthcycle. mthcycle is implemented in main.a and we'll see how it works later. Briefly, though, it reads the new byte value from the cycle button, uses that to look up a new corresponding string title ("January" through "December") and assigns that as the new string pointer for the button. In order to get the button to have the correct initial string title, we also call mthcycle once manually, just after setting its initial byte value.

Instantiating the Go button and the Find button

For buttons, finally, we'll instantiate two standard push buttons. These are just for show, they don't have any value or target assigned. However, the Find button will be assigned the default control state, so you can see how that works.

Instantiating push buttons is more or less the same as instantiating other kinds of buttons. Get the tkbutton class pointer, call tknew, save the pointer to the tstviews pointer store. Call the init method, set the title by calling the settitle method.

Where push buttons differ from radio buttons and checkboxes is that they don't have a state, because they don't get checked on or off. You can assign a value using any of the TKCtrl methods to assign a value, and you can assign a target the same as any other TKCtrl, since a TKButton is a subclass of TKCtrl.

The target/action occurs when the button is clicked. There is one additional way to trigger a button, which we see above. The Find button is assigned the default control flag. This is a flag provided by the TKCtrl class, it's set with #setflag this,cflags,cf_deflt. A button that is the default control takes an alternative color for the default control from the user-defined color theme.

The way default buttons are triggered is a bit complicated, but is quite powerful. Any printable key event handled by the Toolkit environment is sent to the first key view. This could be a TKInput if that field is in focus, but it could also be a TKList, or a TKTable, or something else. If the key event is allowed to propagate, it is passed to the parent view in the node hierarchy. This may be allowed to propagate up to the root view. From there, if the event is a press of the RETURN key, the root view searches down every visible branch of the view hierarchy looking for a default control, and sends the key to that view.

The important part here is that it only searches the visible node hierarchy and it delivers to the first default control. You can mark any control as default, but it only triggers the first one it finds. If you put one default control inside each tab of a TKTabs view, it automatically skips the ones nested within the unselected tabs, because the content views for those tabs are not visible. This would work the same way for other kinds of container views that manage the visible status of multiple child views.

For our two buttons, the offtop is set to 20 for both, putting them on the same row. But their offleft and width properties are adjusted to put them side by side.

Instantiating the fields subtitle

Just like the options and the buttons subtitles, we will create another subtitle for the the text fields. Nothing special here, the title is given an offtop of 22 to leave a space between it and the Go and Find Buttons above.

Instantiating the name label and field

Next in the set of test views in the left hand content view, we will create a series of three text fields, TKInput class, each with a label to the left of the field.

The labels are: Name, Age and Address, but I want them all to be the same length so the text fields line up after them. Therefore, these will be: "Name", "Age ", and "Addr", and note the space following Age so it takes up 4 spaces.

Starting with the name label, this should be familiar. Class pointer comes from classptr, then tknew to instantiate. Store it at tstviews,15 and then initialize. Set its string pointer with the setstrp method. This takes an AX pointer, so after getting the RegPtr from a call to uitest_s, a quick call to xytoax converts the pointer format.

Change the default label anchoring of top, left and right, to just top and left. And then set the offtop to 24, the offleft to 1 and the width we'll set to 4, which as mentioned above is the length of each of the three labels. Append to the content view.

Next we have a class that we haven't seen before, TKInput. TKInput inherits from TKCtrl and provides a single line text input field with many nice features, including automatic integration with the clipboard, input validation and more. This is one of the classes that was loaded and relocated, so it's class pointer is retrieved with #storeget classes,3 and then instantiated with tknew.

We'll save the object pointer to tstviews,18 and initialize with no parameters. Next, we set the TKInput's string. Because it inherits from TKCtrl, TKInput uses the TKCtrl's value type and pointer which are set by the setstr method. TKInput also implements its own version of this method to allow it to take an extra parameter; the buffer length. Recall that we previously initialized a single page of memory to serve as the buffer for all three text fields. In that page, name gets from $00 to $2f. Thus, to set the field's string buffer first prepare the method setstr. Next, load Y (RegPtr high byte) from txtbufpg, load X (RegPtr low byte) with 0, and load the accumulator with $2e. The accumulator passes the maximum permissible string length. Passing one less than the size of the buffer (which is $30), leaves room for the string's NULL terminator which TKInput automatically adds and manipulates.

The TKInput's default anchoring is top and left. But we'll change it to be top, left and right. Set the TKInput's offtop and offleft so it starts a fixed width from the left, leaving one space after the name label. Now that it's anchored to the right, it will flex with the resizing of the vertical splitter. Set its offrght to 1 so that there is a single space, a sort of margin at the right end.

This field has no validation, you can type anything into it. It has no action, so pressing return while in the field doesn't trigger any action. It has default behavior that comes from the implementation of the class, for keyboard and mouse manipulation of the cursor, ability to clear the contents with the CLR key, ability to make selections using LEFT-SHIFT plus cursor keys, and cut, copy, and paste using COMMODORE+X, COMMODORE+C and COMMODORE+V. If you try to paste more data than there is room to fit in the buffer, the paste stops when the buffer is full. You get all of this and more for free.

Instantiating the age label and field

The age label and text field are going to be very similar, but we'll introduce a delegate structure to the text field for getting more control over how the field can be used.

The age label is created exactly the same way as the name label was. Only difference here is that we'll save its pointer to tstviews,16, we'll get its string pointer from uitest_s #15, and we'll set its offtop to 25. Its width is still set to 4, just like the others and to make redrawing simpler, the string itself (found in main.a) is "Age " with the trailing space. This means we don't have to worry about setting the label's opaque flag to prevent ghosting while scrolling or resizing the parent view.

The age text field is created just like the name text field. When it comes time to assign the string buffer, age was given a smaller range in the page of just 16 characters, from $30 to $3f. Again load Y from the txtbufpg, this time load X with $30 (the low byte, the start address in the txtbufpg for the start of age's buffer), and load the accumulator with $0f. That's 15, or one less than the buffer size, to leave room for the NULL terminator.

Next we change the resize mask to top, left and right, then set the anchoring offsets to position this field beside the age label.

And then we have the one difference for this field. TKInput has a property called, idelegs, (input delegate structure.) To this property we assign a pointer to agedel, which is defined in main.a. Set this property using the macro #setobj16 this,idelegs,agedel. We will explore exactly how TKInput delegates work later in this tutorial. Append the view to its container.

Instantiating the address label and field

The final label and text field pair are created just the same as the name label and field pair. Only difference being their offsets, indexes for saving the pointers and fetching the string pointer. And the field buffer size and offset.

Not much to say that is different than the name label and field. When the address field is created, its string buffer within the txtbufpg runs from $40 to $ff, and thus its maximum string length is $bf, leaving room for the NULL terminator.

There is just one additional detail of interest. Because this is the last child view of the left side content view, it is aesthetically nice to have one empty row of margin at the bottom. The TKView, that holds all of these children and which is the content view of the left side TKScroll, automatically computes its own minimum width and height, necessary to contain all its children and their offsets and dimensions. This size is communicated to the TKScroll which automatically adjusts the scroll bars to allow the user to scroll far enough to see all the contained children.

In order to add the extra row of margin, we can simply extend the height of the final TKInput to a height of 2, instead of its default 1. The parent TKView takes this into consideration and tells the TKScroll that it needs to scroll one more row. But when the TKInput actually draws itself, it only draws itself into the first row of its height.


Creating the Right-Side Tab Content Views

Having completed the instantiation and configuration of all of left-side test views, we can now move to the next task. We need to create some views that will be switchable using the TKTabs view on the right side of the split.

We'll create 3 main TKTabs content views, which correspond with 3 tabs: Table, List, and Text. These provide an opportunity to learn how to create a TKTable, a TKList and a TKText view, and to provide each with a datasource and some custom keyboard handling.

Instantiating table columns

First an overview of how a table works. The first view is a TKCols object, which provides the table column headers and the ability to scroll. TKCols inherits from TKScroll. For the main content view of the TKScroll, an instance of TKTable is used. TKTable inherits from TKList. These two are designed to work together to provide the sortable, resizable columns, and for the column headers to stay fixed while the table rows scroll, and when the table view is scrolled horizontally, the column headers are scrolled horizontally to keep them aligned with the columns.

TKCols is not built-in, so we get a pointer to it using #storeget classes,1 and then instantiate with tknew. We have a separate pointer store just for the views that will be managed by the TKTabs, so we store the pointer to this object to tabviews,0.

After initializing the object, we will set a property of the TKCols called colsdef, which stands for columns definition. This is a structure that defines the number of columns and some of their properties. colsdef_ is defined by main.a, because it needs to persist throughout the life of the Application, and we will return to discuss how the columns definition works when we get to its implementation.

The columns have two callbacks, both of which are assigned to routines that are found in main.a. The first is colinfo, which TKCols will call for each column when it needs to draw them. The second is colclik, which is the routine to run when a column header is clicked.

And lastly, we append the TKCols object to the TKTabs view. The TKTabs automatically generates a tab for each of its immediate children. The title of the tab comes from the TKTabs delegate which we'll see later. A maximum of 10 children can be appended to a TKTabs view, for a maximum of 10 tabs. If an 11th child is attempted to be appended, it raises an exception. The anchoring and offsets, and the visibility of the children are managed automatically by TKTabs. Each child is anchored Top, Left, Right, Bottom, and has offsets of 1, 0, 0, 0, so that child views nearly completely cover the TKTabs area, but leave one row at the top to display the tabs. When a tab is selected its corresponding child view is marked visible and the TKTabs' other child views are marked hidden. This is the fundamental behavior of a tab view.

Instantiating the table view

With our TKCols view already instantiated and appended to the TKTabs, we can now instantiate and configure the TKTable view and assign it as the content view of TKCols. And keep in mind that the TKCols is a subclass of TKScroll, so the method setctnt is available on TKCols because it comes from the TKScroll, which we've seen before.

Although TKScroll is a built-in class, TKTable is not, so we fetch the class pointer with #storeget classes,2 and then instantiate it with tknew. We'll save the object pointer to tabviews,1. And then initialize the object.

TKTable has a property called tabdef, which stands for table definition, but it takes the same format as the columns definition, and so we assign the same colsdef_ structure to be the tabdef for the table.

The TKTable has a series of callbacks, not all of which need to be assigned, but we'll assign them all in order to see how they work.


TKTable Architectural Overview

The most important architectural thing to know about TKTable is that it does not store or manage the data it displays. It is a true "view" in the model-view-controller design pattern.1 Although the complete pattern (i.e., fully object-oriented) has been simplified in C64 OS, as only the view is object-oriented.

The table knows how many columns there are and their widths from the table definition structure. This definition can be modified by the Application's business logic (the controller in this case) outside the context or purview of the TKTable itself. When it comes time for the table to draw itself, it only focuses on the cells which are currently visible. The cell coordinates, row and column indexes, are determined by the scroll offsets of the TKTable object, which are managed automatically by the containing TKTCols view.

The table draws one cell at a time, and for each cell that it will draw it calls the ctntaidx (content at index) callback. In that callback, this is the current TKTable object, which the callback can reference if it's used to provide data for more than one table. The row and column index are provided in Y and X respectively. This gives a TKTable a maximum of 256 columns and 256 rows. The callback returns a pointer to the string content for that cell, which it may obtain through any manner. The TKTable doesn't know what the cell content represents or where it comes from.

The table obtains its row count by calling the callback cbk_ctsz (content size.) These two together are required and form the basis of everything necessary to populate a table with content. You never push state information into the table object. You assign its ctntaidx and cbk_ctsz callbacks, you implement those two callbacks, and return meaningful information from them when they are called. The Toolkit then fully manages calls to those routines only when it is necessary.

We will explore how these two callbacks are implemented, later, when we get to them in main.a.


Completing the configuration of TKTable

We set each of the 5 callbacks using the #setobj16 macro. These provide callbacks for content at index, content size, as previously discussed, as well as three optional event handlers: printable key events, click and double-click events.

Next we will set this TKTable object as the content of the TKCols object. To do that we switch the this context to the TKCols object; get a pointer to the object with #storeget tabviews,0 and call ptrthis. Then prepare the setctnt method. Fetch a pointer to the TKTable object with #storeget tabviews,1 and call the method.

With the this context still as the TKCols, we can turn on one or both scrollbars. For this example we'll turn on both. Prepare the setbar method, load X and Y both with the value 1 and call the method. This will allow our table to be scrolled in both directions.



Initializing the list view

TKTable is more complicated than TKList, and since we just saw how TKTable works, TKList works in a similar way but it's simpler and easier to understand.

TKList is the superclass of TKTable. TKList supports a single column, while TKTable takes this core functionality and expands it to multiple columns. While TKTable needs to be appended to a TKCols, (which is a subclass of TKScroll,) to provide the table with the column headers, TKList doesn't have column headers. So TKList can be appended to an instance of TKScroll.

TKScroll is a built-in class. Fetch its class pointer with classptr and then tknew to instantiate it. Save the object pointer to tabviews,2 and initialize the object. Then append it to the tab view, giving the TKTabs object its second tab.

The TKList class is not built-in. Fetch its class pointer with #storeget classes,0 and tknew to instantiate, then store the object pointer at tabviews,3. Initialize the object.

Next we can set the callbacks: ctntaidx (content at index), cbk_ctsz (content size), cbk_keyp (printable key event handler), cbk_clik (click event handler), and cbk_dclk (double-click event handler). Note that these are same callbacks used by TKTable. That's because TKTable inherits these from TKList. When TKList calls these, it provides only the row index, whereas when TKTable calls these, it provides row and column indexes.

Lastly, we need to set the TKList object as the content view of the TKScroll and then turn on the TKScroll's vertical scrollbar. For the list view, we don't need to scroll horizontally. This is accomplished the same way the TKTable was assigned as the content of its TKCols container.

Instantiating the text view

We'll now create the third and final tab content view for the TKTabs which concludes all of the Toolkit class instantiation for this entire Application.

TKText inherits directly from TKView, but it provides a setstrp method similar to TKLabel, except TKText takes a standard RegPtr. TKText has a number of configurable properties; we will start with the default values, but later we will see how another part of our user interface gets connected to this object to change its display properties at runtime.

TKText is an advanced view class with sophisticated back-end data handling. It is capable of displaying ASCII, PETSCII, or MText, which is a lightweight PETSCII markup format with support for text alignment, theme-based text-style colors, and inline links. TKText also supports soft wrapping, breaking lines between words so the text looks natural and easy to read. It accomplishes the wrapping by using an MText-aware wrap library (wrap.lib). The wrap library does not modify the original source text. It builds and provides TKText with a table of pointers into the source text where each new display line begins. This is called a line table. Memory for the line table is managed automatically, but you need to be aware that additional free memory is required for the line table to be maintained.

TKText and the wrap.lib coordinate with each other automatically to generate and update the line table, computing the maximum width and height of the rendered source text. This is communicated to the containing TKScroll object so that the scrollbars are kept up to date while allowing the text to be re-wrapped in response to the changing size of the TKText view, all in realtime.

If you instantiate a TKText, you must delete it later.

Each time a TKText class is instantiated it loads and links the wrap.lib. Therefore it is critically important that each TKText object be deleted during the take down phase of an Application (or Utility), so the wrap.lib gets properly unloaded.

Otherwise your Application will leak the resource of the wrap.lib being loaded. This will eventually lead to problems as a shared library can only be loaded 15 times, before it raises an exception.



The TKText object must be embedded into a TKScroll object, in order to scroll the text content. Start by creating a TKScroll object. It's a built-in class, fetch the class pointer with classptr and then instantiate with tknew. Save the pointer to the TKScroll to tabviews,4 and then initialize the object. Append to the TKScroll object to the TKTabs to create the 3rd and final tab.

TKText is not a built-in class, fetch a pointer to with from the classes pointer store and instantiate with tknew. Save the pointer to the TKText to tabviews,5 and initialize the object.

The wrap library attempts to wrap the text before the TKText has been rendered the first time. The way the Toolkit works, it computes the width and height of each view based on anchoring properties and size of its parent view. When wrap.lib first attempts to wrap, TKText has not yet had its width computed. Therefore, it is necessary to preset some sensible width on the TKText. In this case, setobj8 is used to set the width to 22.

Next set the string content of the TKText object by calling its setstrp method, with a RegPtr to the string. Earlier in this tutorial, we have already loaded a sample text file. The page number to the start of the buffer into which that text was loaded was preserved in the variable, txtbufpg. Therefore, to get the RegPtr to the string content, load the Y register from txtbufpg and set the X register to 0.

As with the table and the list in the previous two tabs, we will now set make the TKText object as the content of the TKScroll. Change the this context to the TKScroll by getting the pointer from tabviews,4 and calling ptrthis. Prepare the setcnt method, get a pointer to our freshly created TKText object from tabviews,5 and call the setcnt method.

There is more than one mode that the TKText can use to present the text, wrapped and unwrapped. When wrapped, there is no need for a horizontal scroll bar. Since the text starts as wrapped, we start by enabling only the vertical scroll bar. Later, interactions with the menu bar and one of the left sidebar checkboxes will be set up to toggle the wrapping mode. This code will also dynamically enable and disable the horizontal scroll bar.



Wrapping up the User Interface

All of the instantiation and configuration of the entire UI is now complete. You can see that it takes a fair amount of code to create, initialize, configure and snap together the objects that make up the user interface. All of this work only needs to be done once, when the Application is first launched. After that, it never gets run or referenced again. By putting all of this one-use code in the special init.o binary, we have saved a lot of memory that can be used for other things during the runtime life of the App.

The last things to cover in part one, initialization, of this tutorial is cleaning up after creating the UI, and some final steps to kick the Application to life. And below that we'll go over a couple of the helper routines that were used to build the UI.

One of the promises of C64 OS Applications is that they remember their state and restore that state when you return to the Application later. This is especially helpful if an REU is not being used and Applications have to load from scratch each time you switch to them.

Remembering state means, in part, that if the user selects one of the tabs and then returns back to Homebase, that when the App is opened again the last selected tab is still the currently selected tab. This gives the user the feeling of stability and makes them feel like they just set the App aside, rather that killing it and coming back to it from the beginning all over again.

We saw earlier in this tutorial how to load the config file. That loaded a selected tab index into the config variables section in main.o. To make use of it, we have to read that tab index and set the currently selected tab from it. Start by setting the this object to the TKTabs view. Load the pointer from the views pointer store and call ptrthis. Prepare its seltab (Select Tab) method. Load the selected tab index from the config variables into the accumulator and call the method.

Loading Icons

TestGround uses a few custom icons. It puts an icon on each of the three tabs, one for a table, one for a list and one for a sheet of text. This is done because it looks cool.

Custom icons on the tabs
Custom icons on the tabs

How the icons are put on the tabs is discussed in part 2 of this tutorial. However the custom icons need to be loaded. C64 OS provides an icon library with many common icons ready to be used. The icon table is in the data structures and content segment of init.o.

At this stage of the App being opned the loader is still showing the Application launching splash screen. The App's icon is shown on that splash screen. That icon occupies part of the custom character set that we're about to replace. We don't want these custom icons to be displayed (even for a moment) in place of the App's icon. That is why loading the icons has been deferred to the very end. It's the last thing being loaded from disk. Just prior to loading the icons, call the KERNAL routine loadclr. This removes the App icon from the splash screen. Then get a RegPtr to the icontab and call loadicns. The App icon will disappear from the splash screen only for a moment before the entire splash screen is replaced by the App's UI.


At the start of initialization we disabled the mouse. This was to prevent the mouse pointer sprites from blinking on and off as data is loaded from disk. We now have to turn the mouse pointer back on with the KERNAL call initmouse.

Initialization used the path library to help us manipulate file references. The path lib is not needed during the lifetime of the Application, so we can save memory by unloading it now rather than when the Application is quit.

Finally, in order to integrate our Application into the OS, we load a RegPtr to the screen layer structure, (which is found in main.o because it persists beyond initialization) and make the KERNAL call layerpush. With the layer on the stack, it will be given an opportunity to draw itself to the screen buffer, at the appropriate times, and it will be given notifications about mouse and keyboard events.

The initialization fully ends, and the App becomes active by marking the layer as dirty, which tells the OS to give the screen a refresh.

Auxilliary Routines

The following are 3 auxilliary routines that were used during initialization.

It just so happens that we create a bunch of objects that all share a common offsets from their parent view. It is, of course, perfectly acceptable to use the #setobj8 macro over and over again to set the left and right offsets on each object. However, they all operate on the same this context, and set the same values. The macros expand to 6 bytes each, and there are 2 of them for 12 bytes each. But it only takes 3 bytes to call the lmargin routine. Therefore, if we're going to repeat this code 30 times, it's 30 * 9 or 270 bytes saved. That's more than a page of memory saved, that also doesn't have to be loaded from disk. This is a simple optimization, nothing more.

Several classes are loaded to be used by this Application. Some are classes provided by C64 OS but that are not built into the KERNAL, and one is special to this App and found in the Application bundle. These two routines are, loadclass to load the OS provided ones and ldlclass to load a local class. It's called local because it's part of the App.

Let's start with loadclass because it is a much more common task. The data structures and content segment of init.o was covered earlier. It holds a pointer store called classes, which provides space for 6 pointers that can be accessed with the #storeget and #storeset macros and the indexes 0 to 5. There are two other variables, csizes and cnames. These provide a list of 6 page sizes, and a list of 6 filename strings preceded by a string accessor macro, #strxyget.

To load a class we need to specify the index into these variables for the class we're loading. Dynamically loaded classes get relocated and then linked to their superclass. The superclass can either by a built-in class, or it can be a dynamically loaded class that has itself already been loaded in and linked to its superclass. It is necessary therefore to load the classes in the order of their dependence. For example, TKTable is a subclass of TKList, which is a subclass of TKView. TKView is built-in. Therefore TKList has to be loaded in first, and then TKTable can be loaded in.

The index to the class being loaded is passed in the accumulator and a pointer to the superclass is passed as a RegPtr. The RegPtr is immediately written to "class" as that is how the superclass linking is performed. The index is used to look up the page size of the class from the csizes list, and that is used to make a page allocation. The returned page number is stored directly to a zeropage pointer.

The path library routine, gopath, can be used to turn any page into a file reference to one of several default places (which it dynamically looks up.) The code "s" is used by gopath to turn the first page of the allocation into a system directory file reference. When that is returned the pathadd routine from the path library is used to append a path string to the file reference. tkpath was also declared in the data structures and content segment of init.o. There is now a page allocation big enough to hold the class, and its first page is configured as a file reference to the system directory's toolkit directory (//os/tk/) which is where all the loadable Toolkit classes provided by the OS are installed.

At this point it falls into an ending that is common to both loadclass and ldlclass. Again using the index that was passed in it looks up a pointer to the name of the class. And the setname routine from the path library is used to write that name into the file reference. The reference is then passed to the KERNAL routine loadreloc. Loadreloc uses the first page as a file reference, but what it loads overwrites that first page and continues writing into the rest of the allocated space. Loadreloc automatically calls the relocatable's link routine. Every loadable class starts with a link routine that performs the work of linking itself to its superclass. It fetches the superclass pointer from class and replaces the pointer in class with a pointer to itself. This pointer is then returned to be saved in the classes pointer store.

The routine ldlclass is nearly the same. It takes the same input parameters. But instead of using the code "s" for gopath (dynamic reference to the system directory) it uses "." which is a dynamic reference to the current Application bundle. Everything else is the same.



Library and KERNAL link tables

An Application that uses the separate init.o binary has two major code sections. The code in main.o lasts in memory throughout the life of the App. The code in init.o lasts only for the initialization and then it's gone from memory. The main part of the App will certainly call some KERNAL routines and may also call some routines from libraries that are loaded. The init part of the code will also certainly call some KERNAL routines and may also call some library routines. However, it is sometimes the case that init.o will call some KERNAL routines that main.o never needs to call. And it may also be the case that init.o loads and uses some libraries that main.o doesn't need.

TestGround shows an example of both of these. Init needs pgalloc, but it doesn't need pgfree. Main needs pgfree, but it doesn't need pgalloc. This makes sense, init is making allocations that it doesn't need to clean up, main will eventually clean things up that it didn't need to allocate, because init did that for it.

At the very start of the Application's initialization it called the system routine initextern twice. Initextern is technically not part of the KERNAL, it's a critical system component that's installed in workspace memory by the booter. The first call initialize the KERNAL link table found at the end of main.o. The second call initializes the KERNAL link table found, above, at the end of init.o. You don't absolutely have to split the KERNAL link table into two. However, above, you can see that 10 KERNAL routines are used by init that aren't needed by main.o. Splitting them into an init KERNAL link table will save 31 bytes of main memory during the runtime of the Application.

Also early in the Application's initialization it loaded two shared libraries. The first, path.lib, is only used by init. After it is loaded, its page byte is filled into library link table shown above. At the end of init.o path.lib got unloaded. Init.o also loaded in memory.lib, and it wrote the page byte into a similar library link table found at the end of main.o. Memory.lib remains loaded after init.o is done. The code in main.o will make repeated calls to the memory library throughout the Application's lifetime. Main.o is responsible for unloading memory.lib.



That ends Part 1: Initialization of this tutorial. The next section covers the main Application's logic. Although you would think that initialization would be short compared to the main Application, believe it or not, it's the other way around. So much of the logic of the user interface is handled by the Toolkit classes that once initialization sets them all up and connects them together, the Application's main business logic is quite short.

Next Section: TestGround Part 2: Main Application


Next Chapter: Writing a Utility

  1. Different people have different ideas about how model-view-controller (MVC) works. The version of this pattern that Toolkit in C64 OS is based upon is the one used by OpenStep, which is at the heart of how AppKit works in macOS (and later in iOS.) You can read more about this pattern in the Cocoa Objective-C Documentation Archive. []

Table of Contents



This document is subject to revision updates.

Last modified: Apr 26, 2024