Skip to content

Guide: the driver model and multiplatform

Every example in these guides ends the same way:

gDriver = new UXAppKitDriver(); // the ONE platform-aware line

A UXKit program differs per platform in one place. This guide covers how to build that place and what happens on the other side of it.

A driver is the backend. It owns four things your code never touches:

windowscreate, open, invalidate, close
the shadow treethe flat object array the platform walks, mirroring your view tree
eventsdecode the native event into a neutral UXEvent
realizationmake native controls for the nodes that have them, and push their state

Everything above the driver (views, controls, layout, the responder chain) is one body of code on every platform. Below it, UXAppKitDriver talks to NSWindow and NSButton, UXWin32Driver to HWND and BUTTON, UXGemDriver to the AES, UXGtkDriver to GTK4, and UXWebDriver to a canvas.

In a real program the one line becomes one file. This is the pattern from the Rocks editor:

#ifdef ARCH_arm64
#import "UXAppKitDriver.xc"
#endif
#ifdef ARCH_arm9
#import "UXGemDriver.xc"
#endif
#ifdef ARCH_win64
#import "UXWin32Driver.xc"
#else
#ifdef ARCH_x86_64
#import "UXGtkDriver.xc"
#endif
#endif
class AppDriver : Object {
static bool start(UXApplication* app) {
#ifdef ARCH_arm64
UXAppKitDriver* d = new UXAppKitDriver();
gDriver = d;
d.setInteractive(true); // GUI: [NSApp run] owns the loop
d.attachApp(app);
return true;
#endif
#ifdef ARCH_win64
UXWin32Driver* wd = new UXWin32Driver();
gDriver = wd;
i32 w = 0; i32 h = 0;
return wd.boot(&w, &h);
#else
#ifdef ARCH_x86_64
UXGtkDriver* xd = new UXGtkDriver();
gDriver = xd;
i32 w = 0; i32 h = 0;
return xd.boot(&w, &h);
#endif
#endif
}
}

Every UXKit gate uses this target-to-platform mapping:

targetplatformbackend
arm64macOSAppKit
x86_64LinuxGTK4
win64WindowsWin32
arm9AtariGEM/AES
ios-simiOSUIKit
wasm32the webcanvas

A driver’s header pulls in its platform’s system interfaces. The Win32 driver names comdlg32, ole32 and comctl32, and the GEM driver names the AES. These do not exist on other platforms, so the #ifdef has to wrap the #import as well as the new.

return wd.boot(&w, &h); // false when there is no display

boot returns false on a headless CI box or when DISPLAY is unset. Treat that as a skip, not a failure. “This environment has no screen” and “this program is broken” are different results, and counting both as failures makes a green CI run meaningless.

AppKit has a different shape: it has setInteractive(true) and attachApp, because [NSApp run] owns the loop and native events have to be routed into your application instead of pulled from it.

Three driver behaviours explain things that otherwise look like bugs.

Native controls are realized, not drawn. On backends with real widgets, a UXButton becomes an NSButton or a BUTTON window. Your drawRect is not called for it, and the platform themes it. A plain view placed “on top” of one in the toolkit’s tree does not intercept its clicks, because the platform hit-tests its own hierarchy first. See UXShieldView for the fix.

A press may never reach the toolkit. Where the control is native, the OS reports BN_CLICKED or an NSButton action, and the driver fires your callback by handle and node, with no synthetic click and no hit-test. Where the control is toolkit-drawn, the press arrives as an ordinary event and routes through the responder chain. Both paths end at the same method, so you write one handler.

A drag is modal. Pressing a scrollbar or a split divider enters the driver’s trackDragStep, which follows the pointer itself and returns when the button comes up. No mouseDragged events are emitted, so a widget that waits for them waits forever, and a recorder sees the press and nothing else. For this reason UXEvent has outcome kinds such as UXEventScrolled.

The driver is an interface, so most of the toolkit is testable headlessly. Geometry, ranges, predicates, text layout, timers and index sets need no driver at all. The examples in these guides that print output run this way.

For the parts that need a driver, AppKit has a capture mode that realizes native controls without a visible window. The portrait sheets on the widget pages are produced with it.

  • UXApplication: the run loop the driver feeds
  • UXViewTree: the shadow tree a driver walks, and why it is the AES object tree on GEM
  • UXShieldView: intercepting input above native controls
  • UXEvent: what a decoded native event looks like