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

Chapter 6: Using Toolkit

This chapter begins with an overview of what Toolkit is and some theory about how it works and how to best make use of it.

The chapter is broken out into additional sections. Toolkit Class Reference gives descriptions and examples of how to use all of the built-in classes. A tutorial for how to load and link runtime loadable classes is included with descriptions and examples for how to use each of the additional classes included in C64 OS. And finally, subclassing covers how you can create your own custom subclasses of existing classes.


What is Toolkit?

Toolkit is a flexible and extensible object-oriented system for building rich and powerful user interfaces with a minimal investment of code and effort. Toolkit, like everything else in C64 OS, is written in 6502 assembly language and is designed to be used and called by your Applications and Utilities which are also written in 6502 assembly language.

One first consideration. 6502 assembly language is not known as an object-oriented language, so how is it possible to program an object-oriented toolkit with just 56 assembly instructions? For a technical description of how object orientation is implemented in 6502, see the weblog post, Object Orientation in 6502 (Take 2).

In short, object orientation is a design pattern. Typically the compiler of an OO language uses the declarations found in headers to enforce certain elements of that pattern, for example, if an object's property is marked as private the compiler will generate an error and refuse to compile if anything attempts to improperly access that property. A 6502 assembler, such as TurboMacroPro, cannot enforce those constraints. However, this does not prevent us from thinking, designing, and constraining ourselves to working with the concept of objects. By following the object-oriented design pattern, we gain several key advantages that are advantageous, especially for user interfaces.

Overview of object orientation in C64 OS

Object-oriented code is not not perfect. Sometimes it can be slow, due to the extensive reliance on recursion and abstraction requiring properties to be accessed via pointers. Object-oriented code can also consume a lot of memory, which the C64 does not have in abundance. Therefore, it is important to be judicial in our designs. The Toolkit is loosely based on the design of OpenStep (the open source version of NextStep, which is the spiritual ancestor of AppKit and UIKit found in macOS and iOS.) However, it has been dramatically simplified, and the runtime has been completely eliminated, making C64 OS objects faster and smaller but more static. Additionally, the only objects in C64 OS are user interface elements. Everything else, such as data storage, and primitives like strings, ints, floats and structs, are non-object-oriented.

There are two entities which must be properly understood: Classes and Objects.

Structure of a Toolkit class

A class is essentially a structured chunk of memory that consists of two 16-bit properties, followed by a jump table to an ordered set of routines. Each of these routines is designed to operate on an object that is an instance of this class or one of its subclasses.

The first property of a class is a pointer to its superclass. The C64 OS Toolkit does not support multiple inheritance (and neither does OpenStep.) Many subclasses may inherit from the same superclass, but every class has only a single superclass, which itself may have one superclass, and so on in a chain of pointers back to the root class. The root class is called TKObj. TKObj is the only class with a superclass pointer of $0000, which establishes it as the root of the superclass chain.

The second property of a class is the 16-bit (little endian) size of the object that is the instance of this class. When an object is created from this class, this size value is referenced and an chunk of memory of exactly that size is allocated (internally by Toolkit) using malloc.

Following these is the jump table to the routines which are the class's methods. TKObj defines the first two methods: init and delete. Every subclass must have at least the same number of methods as its superclass, and they must come in the same order, but a subclass may have additional methods that follow.

Structure of a Toolkit object

An object is essentially a structured chunk of memory that is the size that is specified by the size property of the class of which the object is an instance. The bytes within that struct are defined by the list of properties for objects of that class. Objects of subclasses must have an object size that is at least the size of objects of their superclass, with the same set, order and size, of properties. However, the object of a subclass may be larger with additional properties that follow those defined by the superclass.

TKObj defines just a single property; the "isa" pointer. This is pronounced "is a" not "eyes-ah" or "ice-ah". It's called the isa pointer because if it points to, say, the TKTable class definition, it allows you to say, "This object is a TKTable."

An appropriately sized chunk of memory, with the isa pointer pointing at a class, is by definition a Toolkit object. An object is created by calling the KERNAL routine tknew with a RegPtr to the class for the new object. Tknew reads the size of objects for this class from the class definition, allocates memory of exactly that size with malloc, and sets the first two bytes of that new memory allocation to point to its class.

Initializing a Toolkit object

Tknew allocates memory and sets the isa pointer only. The state of the rest of the memory that makes up the object is unknown, and probably holds values that are not valid for the properties that memory represents.

For this reason, it is almost always necessary to call the init method on a new object. The responsibility of init is to initialize all of the properties of the object to hold valid default values. TKObj defines init, but it does not perform any work. The implementation of init in a subclass typically calls the init method implemented by its superclass. After the superclass's init method returns the values it defined may be modified, and any additional properties that the superclass is unaware of can be set to default values. The process of one class calling its superclass's init method is recursive.

Deleting a Toolkit object

If a Toolkit object is no longer needed, its memory can be recovered by calling its delete method. TKObj implementation of the delete method calls free (the opposite of malloc) to release the memory of the object and make it available again to be allocated for something else.

Sometimes a class will allocate other objects as part of its own behavior. When that class is deleted its own delete method implementation must handle deleting all of the classes that it is responsible for. A class's delete method should always call its superclass's delete method, so that the superclass has an opportunity to clean up its own resources. Eventually, calling the superclass's delete method will reach TKObj's implementation and free this object itself.

Subclasses of TKObj, such as TKView, have properties that allow it to be connected to other objects. These connections must be properly severed prior to deleting an object. If an extant object continues to hold a pointer to an object which has been deleted, this will likely lead to memory corruption and/or a crash at some point in the future.

The THIS and CLASS pointers

All methods are routines which operate on the properties of the current object. The current object is defined by a zero page pointer called this. Prior to calling any object's methods the this pointer must be set to point at the current object. This is done by calling the KERNAL routine ptrthis and passing to it a RegPtr to the object that is to be made the new current object.

Ptrthis additionally reads the this object's class property and sets it into another zero page pointer called class.

Diagram of relationship between classes and objects.
Diagram of relationship between classes and objects

For a deeper technical description of how object-orientation is implemented in C64 OS's Toolkit, review the weblog post Object Orientation in 6502 (Take 2).


The Toolkit environment

A Toolkit environment consists of a set of properties that enable the existence of a Toolkit user interface that belongs to a single process. When an Application and a Utility are open together, File Manager and the Usage Utility for example, each of these has an independent Toolkit user interface, consisting of a hierarchy of nested Toolkit objects. Each of these independent Toolkit user interfaces requires its own Toolkit environment to manage its global properties.

Usually an Application (or a Utility) will have just one Toolkit environment, but it is possible for a single process to manage more than one Toolkit environment.

When a Toolkit class is instantiated it internally uses malloc, a required input parameter for malloc is the memory pool from which to make the allocation. A memory pool must be allocated using pgalloc for each Toolkit environment.

When a Toolkit view hierarchy is instructed to draw itself, each class draws by making calls to the context drawing system. These, in turn, require a draw context. Each Toolkit environment must be assigned its own draw context.

The Toolkit environment is defined by //os/s/:toolkit.s and consists of the following properties:

te_dctx  = 0  ;2 Draw Context Pointer
te_mpool = 2  ;1 Memory Pool Page
te_flags = 3  ;1 Env.Global Draw Flags
te_layer = 4  ;1 Screen Layer
te_rview = 5  ;2 Root view
te_fkeyv = 7  ;2 First key view
te_fmusv = 9  ;2 First mouse view
te_cmusv = 11 ;2 Clicked mouse view
te_posx  = 13 ;1 Ctx2scr X-Offset
te_posy  = 14 ;1 Ctx2scr Y-Offset

The typical way to set up a Toolkit environment is to define the structure in an area of the main binary of the Application or Utility used for state variables and data structures. A draw context structure can be defined, and when the Toolkit environment structure is defined its first property, te_dctx, is a pointer to that draw context.

drawctx  .word 0           ;Char Origin
         .word 0           ;Colr Origin
         .byte screen_cols ;Buff Width
         .byte screen_cols ;Draw Width
         .byte screen_rows ;Draw Height
         .word 0           ;Offset Top
         .word 0           ;Offset Left

tkenv    .word drawctx     ;Draw Context
         .byte 0           ;Memory Pool
         .byte 1           ;Dirty
         .byte 0           ;ScrLayer 0
         .word 0           ;Root View
         .word 0           ;1st Key View
         .word 0           ;1st Mus View
         .word 0           ;ClikMus View
         .byte 0           ;Ctx2Scr PosX
         .byte 0           ;Ctx2Scr PosY

Details about how the draw context works and how to establish its buffers and properties is covered in a later chapter.

The minimum size of the memory pool for the Toolkit environment can be computed by summing the object sizes of all the classes that will be instantiated in the user interface. In addition, there are 3 bytes of overhead for ever malloc'd allocation. Therefore, the pool must include room for those. Take the number of objects that will be instantiated (including objects instantiated by other objects, for example a TKScroll may instantiate one or two TKSBar objects) times 3 bytes each. Round the total number of required bytes up to the nearest page.

If your Application will also use malloc from the same memory pool, the pool can be much larger than the minimum. However, if the memory pool runs out of space and a class being instantiated fails to obtain memory for itself with malloc, an exception is raised and the Application will probably crash.

The te_flags property of the environment should default to tf_dirty, which is equal to 1, to set the Toolkit environment as requiring an initial draw cycle after the Application finishes launching.

The te_layer property of the environment must be set to the screen layer index. This index is returned in the X register by the layerpush KERNAL routine in the screen module. However, when an Application is first launched, its first pushed screen layer is always at index zero. Therefore, this property can be defaulted to zero in the Toolkit environment struct, and only needs to be updated after layerpush if the Application is using multiple screen layers or if the Toolkit environment is being used by a Utility.

All of the remaining properties can be defaulted to zero.

The root view

Every Toolkit environment must have one and only one root view. This root view must be a TKView, but due to inheritance, it may be any subclass of TKView. In practice it only makes sense for it to be a subclass of TKView that is capable of containing one or more child views. For example, although technically possible for a TKButton to be the root view, in practice you probably wouldn't do that.

However, sensible candidates for being the root view include: TKView, TKScroll, TKTabs or TKSplit. Each of these is designed to resize to fill an arbitrary area, and each is designed to take one or more child views and manage or present them in some way.

Only the root view has no parent view. This means the parent property (defined by the TKView class) of the root view is null. All other views in the view hierarchy necessarily have their parent property defined. When a view is sized, positioned and drawn, that is alway relative to its immediate parent view. Because the root view, uniquely, has no parent view, it is always sized, positioned and drawn relative to the whole draw context of its Toolkit environment.

The Position X and Position Y

The X and Y coordinates of low-level mouse events are always relative to the whole screen. When Toolkit processes these low-level mouse events it automatically renormalizes their origin based on the X and Y position of where the Toolkit environment's draw context is composited into the screen buffer.

For most typical Applications the Toolkit environment's draw context fills the screen, and thus its origin is 0, 0. These default values (0, 0) should be set in the Toolkit environment for the te_posx and te_posy properties. These can be adjusted in advanced situations if you want the Application to use some hybrid of manual drawing to part of the screen plus a Toolkit environment in some other part of the screen.

In a Utility, the Utility Framework is provided a pointer to its Toolkit environment, and it automatically manages these positional offsets based on where the user drags the Utility panel around the screen.

Instantiating and using Toolkit classes

Before a class can be instantiated, thus creating a new object of that class, the Toolkit environment must be set. Among other things, this informs the Toolkit KERNAL routines which memory pool is to be used when calling malloc to create new objects.

When in doubt about whether the Toolkit environment is set, set it again. C64 OS is not a multi-tasking environment, therefore, whenever an Application's code is being run, whether its init routine is running, it is responding to a system message, to an expired timer, or one of its screen layer's vectors has been hopped through, that code is run atomically. If you set the Toolkit environment at the start of that atomic code and you can depend on it being set until the end of that routine. If your code ever returns back to the operating system's main event loop, and then flow returns to a different part of your Application's code, the Toolkit environment must be set again prior to working with your Toolkit objects.

It is highly probable that another process will change the Toolkit environment for its own purposes before your code is run again. Therefore, it is essential to set the Toolkit environment again. Failure to do so could result in memory corruption, and things will start crashing.

Set the Toolkit environment by calling settkenv in the Toolkit KERNAL module. Pass to it a RegPtr to the Toolkit environment structure. Such as the following:

	#ldxy tkenv
	jsr settkenv

Creating new objects

With the Toolkit environment set, new objects can be instantiated by calling tknew in the Toolkit KERNAL module. Pass to it a RegPtr to the class which is being instantiated as the new object. The tknew routine returns a RegPtr to the new object. Additionally, the this and class pointers are set for the new object.

How to acquire the pointer to the class depends on whether the class is built-in or whether it has been dynamically loaded. If the class is built-in, its class definition starts some where within the KERNAL's memory space. To fetch a pointer to where this class is located call classptr in the Toolkit KERNAL module. Pass to it a ClassID in the X register. It returns a RegPtr to the class, which is ready to be immediately used by tknew.

The set of built-in classes and their ClassIDs are defined by //os/tk/h/:classes.h The following is an example of how to create a new TKView object:

	ldx #tkview
	jsr classptr
	
	;RegPtr <- pointer to TKView class
	
	jsr tknew
	
	;RegPtr <- pointer to the new TKView object
	;this   <- pointer to the new TKView object
	;class  <- pointer to the TKView class

Calling methods

To call a method, first the this object must be set. If the object whose method you want to call is not the current this object, you can change the current this object by calling ptrthis and passing it a RegPtr to the object. The class pointer is also appropriately set by ptrthis.

Next, the method pointer must be fetched by calling the KERNAL routine getmethod, passing the method offset in the Y register. Method offsets are defined by the class definition headers found at //os/tk/h/. Getmethod uses the offset in Y to read the address of the method from the class pointer, and sets the method pointer into a vector in workspace memory. The method is now ready to be called.

The this and class pointers may not be changed between calling getmethod and then calling that method. After the method is prepared, the registers and other input parameters are prepared, and the current method is called by a JSR to sysjmp. The following is an example of how to call an object's init method:

	ldy #init_
	jsr getmethod
	jsr sysjmp

Although it may seem strange that getting (i.e., preparing) the method is a separate step to be taken before calling the method, it makes more sense when the method uses several registers as input parameters. The following is an example of setting a TKDatePick object's current date:

	ldy #setdate_
	jsr getmethod
	
	ldy #2024-1900  ;1-byte representation of 2024
	ldx #1          ;January
	lda #31         ;31st
	
	jsr sysjmp

Setting properties

The current object is pointed to by the this pointer, which is a standard zero page pointer to what is effectively a data structure (with certain constraints.) Therefore, properties can be modified on the object by using the Y register and the indirect indexed addressing mode.

Some standard macros can be used to make your code more readable and minimize bugs. These macros are defined in Chapter 3: Development Environment → Common C64 OS Macros.

The flag manipulation macros, setflag, clrflag and togflag are ideal for flipping individual bits on the properties of an object. For example, to set the current TKView object's dirty flag the following macro can be used:

	#setflag this,dflags,df_dirty

This has a certain object-oriented feel to it. It is the equivalent of:

	this.dflags |= df_dirty;

The getter and setter macros, although they can be used on any struct with a zero page pointer, they were created with Toolkit objects in mind. This is the reason they contain the word obj. For example, to set an object's 8-bit property to a static value, the following macro can be used:

	#setobj8 this,tag,"a"

	or

	#setobj8 this,bgcolor,c_purple

In an object-oriented way of thinking, this is the equivalent of:

	this.tag = "a";
	
	or
	
	this.bgcolor = c_purple;

To set 16-bit properties to a static value the following macro can be used:

	#setobj16 this,cbk_clik,doclick
	
	or 

	#setobj16 this,height,950

16-bit properties are, like 6502 pointers, little endian. Therefore, the #setobj8 macro can also be used on a 16-bit property to only affect its low byte. This can save time and memory when adjusting a property that, in a given context, is only in the 8-bit range even though the property more generally supports 16-bits. An example of this is the width or the anchored offsets of a view. The anchored offsets initialize to zero; to adjust an offset to a value less than 256, it's not necessary to use #setobj16 as that would explicitly but unnecessarily set the high byte zero.

There are two additional macros to set a 16-bit property to a dynamic value. The first reads the value from pointer and writes it to an object. In the example below, we imagine that a 16-bit width has previous been saved to a block of config data. After that block of config data has been read in, the property of an object can be set to the value in that config data all with a single macro.

	#setobjptr this,width,config+width_

The second macro writes the current RegPtr (X/Y registers) to an object. This gives the flexibility of reading some data from some more dynamic place, including from the property of another object, into a generic RegPtr. And then writing that to an object in a second step.

	#rdxy ptr,height
	
	#setobjxy this,offtop

Getting properties

Getting an 8-bit property from an object is matter of using the 6502's indirect indexed addressing mode on the this pointer. Such as the following:

	ldy #bgcolor
	lda (this),y

There are also some macros to facilitate reading 16-bit properties from an object. The first example reads a property from an object into a RegPtr or RegWrd. Remember the only difference between a RegPtr and a RegWrd is the conceptual meaning of the data. A RegPtr is a 16-bit address, a RegWrd is just a 16-bit number that could represent anything else such as a height, length, or width.

	#rdobj16 this,width

One final macro makes it easy to read a 16-bit property and store it at some fixed address in memory. Such as in the aforementioned example of a width that is read from a block of config data. This macro could do the reverse of that; read the width property from an object and write it to the config data:

	#getobj16 this,width,config+width_

Macros are the fastest and most direct way to access a property on an object. However, macros also expand into code that may be repeated unnecessarily. In some circumstances it may be advantageous to save a bit of memory at the cost of slightly slower execution time.

The Toolkit KERNAL module includes the getprop16 routine. Unlike the #getobj16 macro, this routine can only be used on the this object. The Y register is used to pass the index and the result is always returned in a RegWrd. If used repeatedly it will save memory. The following is the expansion of the #rdobj16 macro.

	ldy #width      ;2 bytes
	lda (this),y    ;2 bytes
	tax             ;1 byte
	iny             ;1 byte
	lda (this),y    ;2 bytes
	tay             ;1 byte
	
          ;total:  9 bytes

The following is the memory usage for the same result by using getprop16:

	ldy #width      ;2 bytes
	jsr getprop16   ;3 bytes

          ;total:  5 bytes

Getter and setter methods

Setting properties using macros or by manually using indirect indexed addressing is very low-level. This makes it fast and efficient, but there is no way for an object to automatically be made aware of the change. For this reason, if a property is set directly, it is often also necessary to set some flag on the object to indicate the change. For example, if a view's background color property is changed, to force the view to redraw with the new color it is also necessary to set the view's dirty flag. (It is also necessary to set the entire Toolkit environment's dirty flag, which is discussed later.)

	#setobj8 this,bcolor,c_yellow
	#setflag this,dflags,df_dirty

In some cases it is necessary for the object to be made aware of a change to one of its properties so that further steps or data processing can be immediately performed. In this case the class implements a setter method. If a setter method is available, it should always be used rather than setting the object's property directly. For example, TKScroll is a subclass of TKView. Under the hood, a child view is appended to a TKScroll the same way it is appended to a TKView. Usually the appendto routine in the Toolkit KERNAL module is used. This is a convenience routine that ultimately just manipulates the parent, child and sibling properties of both objects. However, TKScroll needs to know when a child was appended to itself so that it can adjust that child's anchored offsets and read properties off the child and use them to adjust the scroll bars.

For this reason, TKScroll provides the setctnt_ setter method. This method should always be used to append a scrolling content view to a TKScroll, rather than using appendto. The following demonstrates this:

	ldy #setctnt_
	jsr getmethod
	
	#storeget views,4  ;a reference to a previously instantiated object.

	jsr sysjmp

Before setting an object's property manually, first check if its class header defines a setter method. If a setter method is available, use it instead. Another common example, TKView provides the mechanism for an object to be the first recipient of key events. To make an object the first key view, you should only ever call its setfirst_ method. Although this method does set a property flag on the object, it also performs other work, such as looking up any previous first key view and asking it to resign first key, and then updating the Toolkit environment with what view is first key.

	ldy #setfirst_
	jsr getmethod
	
	sec ;set as first key view
	jsr sysjmp

Although it is less critical, some classes also provide getter methods. If a getter method is available, it is often easier to use the getter method than to read the properties manually. It is sometimes the case that the property data undergoes some transformation before being returned from the getter.

For example, the TKDatePick class uses the composition design pattern. When the TKDatePick is initialized, it creates three TKButtons, configures them as cycle buttons for year, month and date and then appends them to itself. To fetch the current year becomes complicated as it is not stored a simple integer property. Calling the getdate method is a significantly easier way to fetch the year, month and date all in a single call.

	ldy #getdate
	jsr getmethod
	jsr sysjmp
	
	;Y <- Year  ($00=1900,$ff=2155)
	;X <- Month ($01=January)
	;A <- Date  ($01=1st of month)

Toolkit Class Reference

Toolkit consists of a number of built-in classes, plus a growing number of runtime loadable classes.

Next Section: Toolkit Class Reference


Next Chapter: Writing an Application

Table of Contents



This document is subject to revision updates.

Last modified: May 27, 2024