NEWS, EDITORIALS, REFERENCE
A few housekeeping issues to cover up front. I want to say, welcome back, because it has been an unusually long while since my last blog post. I absolutely do not intend for this to become a habit. A few things have conspired against me to make this post a while in the making.
It is the late summer, so I had a bit of a holiday, which I thoroughly enjoyed. It took me and my kids and in-laws to a cottage and away from the internet for a few days. But on the whole it didn't keep me away from my c64, as we'll get into in this post. This website suffered some down time during the past week. I believe it was down for approximately 4 days. I wish to extend my sincerest apologies for the outage. We were actually without internet service for the entire labor-day weekend. This is annoying for me, no doubt, but it also happens to be annoying for you too, if you're trying to get to one of several sites that I host locally.1 And as is inevitable when the kids go back to school they brought home novel illnesses that have kept me away from work for a couple of days.
On the bright side, I've been hard at work on C64 Luggable. The documentation of which is coming along quite well. And I've taken many many photos of more recent work on the project than I have yet had time to document. And I've been working away on new additions for the Commodore 8-Bit Buyer's Guide. I have well over 35 (!) new products, parts and components that I will be adding to the guide. Including the catalog items from Poly.Play and Retro Innovations, and a number of independents from AmiBay.org, Lemon64, and ebay.
And let's not forget all the work I've been doing on C64 OS too. It's been a really fun adventure so far. I am continuing to learn about 6502 coding along the way. The Toolkit, as we'll see in this post, involves a lot of 16-bit math, and much of it on values accessed through object pointers, as I began to discuss in that post. I'm sure I'll get into more of that as I continue to discuss the Toolkit.
What is a toolkit?
An operating system is made up of many parts that take care of all sorts of tasks for the application developer. Memory management, abstract file access, string manipulation, input drivers, networking, etc. The toolkit is the part of the OS that helps an application build its user interface. The concept of a toolkit is probably the component that is the most absent from the C64's (and PET, VIC-20 and C128's) built-in operating system. GEOS, on the other hand, offers services for producing menus, both horizontal and vertical, buttons, dialog boxes, actionable icons, single line text input, and well, that's about where its toolkit ends. But that's a big step up from nothing. The toolkit-like features of GEOS are what help to give every GEOS application a standard look and feel.
On Linux, which runs on PCs with much more memory and processing power, toolkits can be dynamically loaded and different applications based on different toolkits can all be running side-by-side. This is actually detrimental to the Linux desktop experience because not all apps feel as though they belong to the same environment. On Windows or macOS, which have a dominant vendor-supplied toolkit, virtually all apps on those platforms use the standard toolkit and consequently feel much more like they belong together.
Because the C64's OS (Kernal and Basic rom) do not provide services for producing a user interface, everyone produces there own. Amongst the chaos there are a few OSes, the applications for which usually (but not always) feel as though they belong to that OS. C64 OS will fit into this latter category.
What does the C64 OS toolkit do?
Unlike on Linux where multiple different toolkits can be and often are available to applications simultaneously (GTK, QT, See: Wikipedia Article on Cross-Platform Toolkits), the C64's small memory size means it is only practical for there to be one toolkit available at a time. And in most cases that toolkit is tightly integrated with the other features of the OS anyway. There is no way, for example, to swap out the UI drawing features of GEOS.
The Toolkit in C64 OS is a module, but its location in memory is fixed, and the objects it produces are designed to interact with other services of the OS. For example, it gets its events from the main event loop in the screen module. The structure of the events it expects are built by the updatemnk routine in the input module. It makes calls to allocate memory for itself to the memory module. All the while drawing itself with a drawing context system also provided by the screen module. The essential behavior of an application is to link its functionality to the actions of toolkit objects that interpret the flow of events being generated by the input devices. This is also what we mean by program flow being event-driven.
The Toolkit is object oriented. I began to discuss how one can go about writing code following an object oriented design pattern in 6502 in a previous post, Object Orientation in 6502. In that post I began to talk about the Toolkit in order to have examples. Object oriented code is, by definition, structured as a hierarchy of interrelated classes. Sub-classes descend from other classes and inherit and extend their functionality. In rich modern UI Toolkits, such as the Cocoa framework of macOS, or Cocoa Touch of iOS (its little brother), there are literally hundreds of classes. And each class has hundreds of methods (related functions which operate on the object's own properties). UIButton, in Cocoa Touch, for example, has 32 methods. But these do not include the 21 methods it inherits from UIControl, nor the ~164 (!!!) methods it inherits from UIView.2 And so on up the inheritence chain to UIResponder and UIObject. Even declaring this number of methods would overflow 64K of memory before we got around to implementing anything. And a toolkit is only one part of what it takes to make an operating system.
Needless to say, the C64 OS Toolkit is very trimmed down compared to what we might otherwise bemoan as the endless bloat of modern toolkits. But the principle is similar. Toolkit is a collection of classes, some of which descend from others. They work together allowing a programmer to create and assemble them to construct a flexible user interface that efficiently redraws itself, responds to user input, and calls back to application code when necessary to implement the specifics of the program. The Toolkit relieves the application developer from a huge burden of effort and results in more consistency across applications and enables rich functionality for free3 in the process.
As of the time of this writing, Toolkit consists of just 6 classes. These have already been briefly discsussed in the earlier post on Object Orientation in 6502. I have several other classes planned, checkbox will likely descend from Button, radio will likely descend from checkbox, and a multi-line text view is an absolute must-have, but I haven't yet figured out where in the hierarchy it will fit.
This lean (by modern standards) class hierarchy may look unimaginably small, but I think you'd be surprised how much UI complexity can be constructed out of such an essential core.
A sample of UI, extruded to show the nesting of views, and labeled.
All Toolkit classes descend from View. View provides several collections of properties and methods which make it a foundation of several types of functionality. We'll just dig right into those here.
A view-based user interface is a hierarchical tree of nodes. Therefore, each class that participates in the UI needs to be a type of node that can connect to the others to allow application code and other toolkit built-in functionality to navigate the tree. The View class therefore provides node properties. And since all Toolkit classes descend from view, all Toolkit classes have these properties and are all therefore types of nodes. The node properties are as few as I believe it is possible to have and still have it work. Each view has a parent pointer, a first-child pointer, and a next-sibling pointer.
Don't confuse the node hierarchy with the inheritance hierarchy pictured above. In a real UI views will be nested within views, and buttons may be stacked one above the other or put inside scroll views. Labels will be put before inputs and so on. The inheritance hierarchy is hardcoded and never changes, but the node hierarchy of a UI could be totally different for every application.
Every node has exactly one containing parent node. The parent node pointer points to that containing node. There is always one root view, which usually fills the screen, but doesn't have to, and has no parent node. Its parent node pointer is null, ($0000), which allows code that navigates the tree to know when it has reached the root view.
Any node can theoretically contain multiple child nodes. However, only the View class has draw logic which is designed to deal with multiple children. Typically if you want a node to handle multiple children you would rely on the View's implementation because it's long and complex. Each node has a pointer only to a single child node. But if that child node is one of many children of the same parent, then the first child uses its next sibling pointer to link to the parent's second child. The second child can link to the third and so on. Each child points back to its parent even if it is not the first child. The last child of a parent is the last node in a chain of siblings, it has its next sibling pointer set null. This is how code can determine that it has reached the final sibling.
If a node's first child pointer is null, it has no children. If a node's first child's next sibling pointer is null, then the parent has only one child. And so on. These three pointers are enough to describe the entire tree and allows recursive code to navigate the entire tree. Navigating up the tree is very efficient because every node has a pointer directly to its parent all the way back to the root node with a null parent pointer. An advantage to one child pointer and sibling pointers is that we don't need to have an intermediate data structure such as an array to hold an ordered set of child pointers.
Above is visualized the node hierarchy of a very plausible C64 OS application UI. Note that the node hierarchy defines the structural relationship between the user interface objects, but alone it doesn't define where on the screen siblings will display relative to each other. That part comes in the next section about view metrics.
Here we see that there is a single root view, in the top row. Its parent pointer is null. It has three children, a Scroll view, and two more Views. But the root view only points directly to the Scroll view. The other two Views are linked horizontally as siblings to the Scroll view. The rightmost View in the second row is the last child of the root view, so its next sibling pointer is null. The Scroll view has one child, a multi-line text view. The middle View has four children, two Labels and two Inputs. Presumably these would be laid out on screen such that each Input has one Label. The final View has three children, three Buttons.
The node properties are merely structural. In addition to structure each view needs to know where it should position itself. The positioning of a node is always relative to its parent. In modern UI Toolkits, I'm most familiar with Cocoa and Cocoa Touch from macOS and iOS respectively, views support special layout constraint objects. This allows a node to align, position and size itself relative to not just its parent but to its siblings as well. While this does enable the production of incredibly flexible and responsive layouts, C64 OS's Toolkit won't implement anything like that. Firstly, it's far too complex for the C64's memory constraints, and secondly, the size and orientation of a C64's screen has been what it is since 1982. There would be little advantage to having a complex system of constraints meant to adapt a UI flexibly to a variety of different screen sizes and orientations.
Each view must be able to describe its size and position, relative to its parent. To handle this the View class provides several metrics properties, which all other Toolkit classes inherit from View. These properties are:
First let's look at view_rsmask. This is a bitfield for flags that affect resizing behavior. At the time of this writing, the low nybble is well defined, but exactly how the upper nybble is being used is still in flux while I write the code so I won't talk about those yet. Here are the values for the lower four bits.
- %0000 0001 — rs_ankt
- %0000 0010 — rs_ankb
- %0000 0100 — rs_ankl
- %0000 1000 — rs_ankr
These flags stand for: Anchor Top, Anchor Bottom, Anchor Left, and Anchor Right. These declare which sides of the view have a fixed offset from its parent. These define which of the other 8 metrics properties are pre-defined and which are computed dynamically. So let's look at some examples of how this might work.
A view must have at least one vertical anchor, and at least one horizontal anchor. If no vertical anchor is set it defaults to top and no horizontal anchor defaults to left. When the view is anchored, say, to the top, the view_top property defines the distance (in 8x8 cells, not pixels) that the view's top edge sits down from the top edge of its parent. These values are all unsigned 16-bit, so a view cannot be offset negatively from its parent. A view can either be flush with the top of its parent or any offset down, upto 65535 text rows, from its parent's top.
If the view is anchored top, but not bottom, then its view_hght property must be set and is used to figure out how tall the view is. In such a case, vertically resizing the view's parent has no affect on its own height. The situation is similar if the view is anchored bottom but not top. The view_bot property holds the number of rows that this view's bottom edge is positioned up from the bottom of its parent. The view_hght is still relevant to determine how tall the view is and it is still unaffected by vertical resizes to its parent.
If the description is hard to follow, here's a visualization that should help.
The anchor flags can be OR'd together, of course. So rs_mask could be: rs_ankt | rs_ankb.
Things get more complicated when a view is anchored both top and bottom. You can see how this works in the third example above. view_top defines how far its top edge is from its parent's top, and view_bot defines how far its bottom edge is from its parent's bottom. But when the view is anchored on both sides then when the parent is resized vertically the height of the view changes, tracking the height changes of its parent.
Whatever way the view is anchored, some of its properties get computed automatically. Let's start with view_hght. If the view is rs_ankt | rs_ankb, the view_hght is computed and set automatically. When it comes to actually drawing the view, what the drawing code really needs to know is the absolute top and absolute bottom offsets, from the draw origin, for where the view will render after any anchoring and positioning logic has been applied. As it happens the draw origin is at the top,left of any given on-screen rectangle. The reason for this is because of the way the VIC-II's memory addresses map to positions on the screen. The top,left corner of the screen is the smallest memory address. And the bottom,right corner of the screen is the biggest memory address. This is true for any arbitrary rectangle you draw on the screen. The top,left corner of that rectangle will always have the smallest memory address of any address within that rectangle.
The consequence of this is that view_top, is both the relative offset from the top of the parent, but it's also the absolute top of the view from the draw origin. This is not true of view_bot. view_bot is relative to the bottom of the parent. So the smaller the view_bot value, the lower that edge goes on the screen. A view_bot value of 5 says nothing about where the bottom edge of the view is going be relative to the draw origin. That's what view_abot is about. view_abot stands for absolute bottom, and it is always a computed property. If the view is rs_ankt, then view_abot is view_top + view_hght. If the view is rs_ankb, then the height of the parent has to be taken into account. view_abot is parent->view_hght - view_bot. And view_top is then computed as view_abot - view_hght.
At the end of the day, no matter how the view is anchored, the resizenode routine (called internally, never manually), makes sure that all 4 properties are set correctly: view_top, view_bot, view_abot, and view_hght. After this point, the drawing code can completely disregard any positioning complexities due to anchoring and offsets. It simply draws the view between view_top and view_abot, and view_hght is readily available as a reference. Drawing is way beyond the scope of this post, however, and I'll have to return to it at a later date.
I intentionally limited myself in the above description only to the vertical sizing and anchoring properties. The horizontal sizing and anchoring work in exactly the same way. view_left is analogous to view_top. It is relative to the left side of the parent, but also is the absolute offset from the left coordinate of the draw origin. Therefore a view_argt (absolute right) is computed depending on how the left and right anchoring are configured. The logic here is exactly the same, just along a different axis. And of course, rs_ankl and rs_ankr are represented by different bits in the view_rsmask than rs_ankt and rs_ankb, so all 4 can be set independently and simultaneously.
One more brief note before moving on to a different topic. The above description does not even attempt to broach the issue of scrolling, and what effect scrolling has on the calculation of where a view's children will render, or how they get clipped if they are only partially visible. This is of course all taken into consideration in the design of Toolkit, but is way beyond the scope of this introduction.
As we'll no doubt see in concrete examples in future posts, I believe this anchoring and offsets system (which is based loosely on springs and struts which predate autolayout constraints in cocoa/cocoa touch), can be used to make user interfaces that are very flexible. Probably more flexible than anything else available on the C64, and yet simple enough to be eminently suitable for use in C64 OS.
One of the main talking points I use when describing C64 OS is that it is event driven. Part of what this means has already been discussed in an earlier post about The Event Model. The IRQ service routine, which in C64 OS is implemented in the service module, updates the mouse and keyboard which converts user input activity into 3 queues of input events. Mouse events, Key Command events and Printable Key events. The mouse events are particularly relevant to Toolkit because the mouse cursor is passing over Toolkit views rendered on-screen, and the user is clicking on those views feeling him or herself to be interacting with them.
However, mouse events contain only: screen coordinates, key modifier flags and an event type. Somehow a simple event struct like this has to convert into meaningful interaction with the underlying on-screen display.
The Toolkit needs to be able to determine the target view, which is the first view that has an opportunity to do something with the event. And then each view needs a way to figure out how to pass notification about the existence of the event if it can't or doesn't handle it itself. This behavior is called event propagation.
Every class in the Toolkit needs to know how to, at a minimum, propagate an event to another view. And so for this reason the View class has a set of methods for event propagation. And every other view subclass automatically inherits the basic ability to propagate events.
To handle the first requirement, Toolkit has a hit test routine. This routine effectively walks the node hierarchy, recursively, searching for the uppermost view that is rendered under the screen coordinates where the mouse event occurred. This is actually easier than it sounds. All of a view's child views are constrained (clipped even, when drawn) to within the bounds of their parent. Therefore, if the mouse event does not fall within the bounds of a given view none of its subviews, or their subviews, etc. need to be checked. Siblings must be checked, however, and it is possible for two sibling views to overlap each other.
This introduces a small complication, because first children are drawn first, and their siblings are drawn later. This means if a late drawn sibling has view metrics that cause it to overlap with with an earlier sibling, it will render above that earlier sibling. What this means is that when doing hit testing, the walking routine must start with the last sibling and if the event did not occur within its bounds the hit test should then move to the previous sibling. Otherwise, sibling 1 could claim the hit, even though it is covered by a section of sibling 2 that the user believes he or she is clicking on. See the visualization below to understand how this works.
In the example above, first and second child are two children of the same parent. Their metrics are such that their bounds overlap. The red point represents where the user clicked. That coordinate is technically within the bounds of both children. However, because the first child renders before the second child, the second child draws itself overtop of the first child, where they overlap. Thus, when performing the hit test, it is necessary to test the second child prior to the first child.
Actually propagating the event is the second requirement, once a final leaf node (a node which has no children of its own) is found to be the target that node needs to get the event. In C64 OS, the event data itself is not passed, i.e. it is not copied in memory, to the target node. The node is merely notified that an event is targeting it. Toolkit does this by calling the appropriate event routine. The View class has 3 event handling routines, one for each of the 3 event types, mouse, key command and printable key. In our example of the mouse event, then, the Toolkit calls the target node's view_mouse routine.
This merely informs the view that a mouse event is available. If the view handles mouse events in some way, then it can call readmouse in the input module. This will give it the current mouse event details. I'll discuss this further in the responder section below.
By default, the View class is mostly just a generic superclass and container for multiple children. The default implementation of view_mouse is not to read or analyze the event details at all, but just to propagate the event to the next view. It does this simply by calling view_mouse on its own parent in the node hierarchy. This continues up the node hierarchy until a node either handles the event and returns without propagating it further, or the root node is encountered. The root node has no parent node to propagate the event to, so it simply returns.
Key events, both Key Commands and Printable Key events, are handled similarly to each other. They inherently do not contain screen coordinates, so they do not target arbitrary views in the node hierarchy. Instead, they target special views that claim keyboard focus. Only one view may claim focus at a time. Toolkit maintains a pointer to that view, if no view wants to be in focus, or if a view in focus wants to lose its focus (also lovely said to blur) then the root view is assigned as the focus view.
A lower level than the Toolkit, the screen module, which implements the main event loop, also divides the screen renderer into 4 compositing layers. The top most (4th) layer is fixed as the layer onto which the menubar (with CPU busy indicator, top level menus, and clock) and any pull-down menus are rendered. This causes them to always render above everything else. The lower three screen layers can be pushed and pulled by the application from a 3-layer stack. Typically the application's initialization routine pushes layer 1 onto the screen stack. Each screen layer struct has pointers to 4 routines:
During initialization, the application, if it makes 100% use of the Toolkit, can wire these four routines directly to the four equivalent routines of Toolkit. So that when the main event loop interacts with the low level screen compositor the Toolkit routines are called directly without needing the application to intermediate. The separation of screen compositor and Toolkit is to allow a more advanced application to completely forgo use of the Toolkit and handle the low level events manually (most useful for a game or other special purpose), or to share responsibility with the Toolkit. The application can get the low level events first, do with them as it pleases and choose when and if to forward them to the Toolkit.
That was a bit of a tangent. Regardless of how the Key Command and Printable Key events are handed over to the Toolkit, it directs them to the view in focus. And does this by calling that view's view_kcmd or view_kprnt routine respectively.
These routines, in their default implementation by the View class, do exactly what view_mouse does. They merely propagate the events, passing notification, by calling the same routine on their parent node, until something handles the event or the root node is encountered.
The last topic I'll cover in this Toolkit Introduction, is responding. As we saw in event propagation above, although the events are propagated through the node hierarchy, the default behavior, implemented by the View class, doesn't do anything. Except pass the notification to the next view which does equally little. The question is, how does anything get accomplished by means of these events?
All Toolkit classes descend from View, and so they inherit this do-nothing-but-propagate behavior for free. Some subclasses however are meant to respond to events. In some cases this response behavior is entirely internal to the class, requiring no special involvement from the application code. An example of this is an input field. An input view is meant to allow the user to input and edit the text that the field manages. If a mouse event is propagated to an input the input reads the mouse event details from the screen module. If its type is a click event, it tells the Toolkit that it should be the new focus view for all incoming key events. And it configures the system's cursor settings so the cursor begins blinking in the right place, and it returns without propagating the click event any further.
Input events also have a disabled flag, however, which can be configured by the application's code at runtime. If the input receives notice that a mouse event targets it, its first checks its own disabled flag. If it is disabled, it calls its superclass's implementation of this routine. This is very likely to be View's implementation, which as we already know, propagates the event to the input's parent view. Neither of these responses to the event necessitate calling back to the application.
Other subclasses of view, such as button, have both internal and external responses to mouse events. When the left mouse button goes down, it generates a leftdown event. This event is propagated, but when a button gets this event, it marks itself to be redrawn, and when it redraws it redraws with an inverted color. This is an entirely internal behavior, it requires no interaction with the application to appear to be highlighting in response to the user mousing down on it. Similarly when the button goes up, a leftup event is generated. (How mouse up events are routed is a bit complicated, and out of scope for this introduction). The button responds to a leftup by redrawing itself without the inverted color, thus appearing to no longer be highlighted.
Buttons are different than Inputs in at least one important way though, when the input was clicked it prepared itself to handle key events, which is an internal change. However, a button has no inherent, internal change as a result of being clicked. The application needs to be informed that the button was clicked. For this reason, the button subclass adds an additional but_clicked property. This is a pointer to a routine. When the application is being initialized, or at some later time in response to some other activity, the application sets the button's but_clicked pointer to a routine the application has implemented.
The button's own reimplementation of view_mouse receives all mouse events as part of the node hierarchy event propagation system. But button's implementation of this routine calls readmouse to get the event details. If it's leftdown or leftup it changes its highlight state as described earlier, but if it's leftclick it checks to see if its but_clicked property is set. If it is, it calls it, passing a pointer to itself in .X and .Y. And then returns without propagating the event.
The application's own routine is now running. It can, in the simple case, do some fixed behavior, something that should always be done as a result of this button having been pressed. Under more complex conditions, more than one button may be set up to call the same clicked routine. The routine needs to distinguish which button was clicked that triggered the call. It can do this by comparing the pointer to the button in .X and .Y with some reference the app maintains to the that button, or it can write the pointer to a ZP address and lookup and access properties on the button itself. This could be used to change the button's text, or change its hidden state, or change its layout metrics, or read its TK_ID to figure out by ID which button this is to figure out what to do next.
Okay, so that concludes this rather long blog post that I've been putting off writing for so long as the Toolkit module has been under such active design and development. In fact, just in writing this post it's given me some ideas about how to improve the inheritance model.
Inheritance was the topic I left off at the end of Object Orientation in 6502 with just a little preview. The truth is, I hadn't quite figured out in my head the full logic of how it would be possible, in your own application, to create custom subclasses of Toolkit classes. But I think I'm almost there.
So, expect that post to be coming sometime in the not too distant future, a part two to Object Orientation in 6502. And after that it will make sense to return to look in more detail at how some of the Toolkit views make use of their object orientation, especially when it comes to drawing themselves.
Lastly, I suspect large swaths of this post will eventually make it into the C64 OS technical documentation. Especially the images I put together to illustrate some of the ideas.
- Although it's neither the fastest nor the most reliable way to host a website, I host many websites on my own hardware and internet infrastructure, because it's something of an enjoyable hobby for me. I offload heavy assets to an AWS S3 bucket to distribute load and cut down on bandwidth costs. This doesn't help when we lose internet altogether as was the case this past week. [↩]
- It's a bit tricky to tell the difference between a method and a direct access to a property in modern languages like Swift. The notation to read a property is exactly the same notation as to call a method. And the underlying implementation can be changed from a direct read to a method call at any time without changing the code that accesses it. 164 comes from my rough count in the documentation, and includes property accessors. [↩]
- Every feature built into the toolkit classes are features that don't have to be explicitly implemented by the application. Yet they automatically avail themselves simply because the UI is built from these standard classes. Think about momentarily inverting the color of a button to provide feedback when it is clicked. That comes free in every C64 OS application. [↩]
Do you like what you see?
You've just read one of my high-quality, long-form, weblog posts, for free! First, thank you for your interest, it makes producing this content feel worthwhile. I love to hear your input and feedback in the forums below. And I do my best to answer every question.
I'm creating C64 OS and documenting my progress along the way, to give something to you and contribute to the Commodore community. Please consider purchasing one of the items I am currently offering or making a small donation, to help me continue to bring you updates, in-depth technical discussions and programming reference. Your generous support is greatly appreciated.
Greg Naçu — C64OS.com