Skip to content

Guide: the driver model and multiplatform

Every example in these guides starts its application the same way:

UXApplication* app = new UXApplication(); // with the driver for the target this is built for

A UXKit program names no platform: with UXPlatform in the build (or #use <UXKit>), a new application installs the driver itself. This guide covers what the driver does and what you can see 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.

UXPlatform picks the driver from the target the program is compiled for, and imports only that one, because a driver’s header names its platform’s system libraries (the Win32 driver names comdlg32, ole32 and comctl32, the GEM driver the AES), which do not exist elsewhere:

targetplatformbackend
arm64macOSAppKit
x86_64LinuxGTK4
win64WindowsWin32
ios-sim, iosiOSUIKit
androidAndroidAndroid views
wasm32the webcanvas
arm9AtariGEM/AES

Two choices are not the target’s own and are made when building: -D UX_GTK builds the GTK driver for a Mac, against its GTK 4, and -D UX_GEM builds GEM for a Mac, against the host GEM stack.

new UXApplication() installs the driver and tells it about the application, which is where each driver sets itself up: AppKit hands the loop to [NSApp run] and routes native events into the application, iOS and Android hand it to the system’s own loop.

app.run() starts the driver. When there is no display to talk to (a headless CI box, an unset DISPLAY) it returns 1 without running. 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.

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