Skip to content
This is the documentation for xc 0.64. The current documentation describes the newest version.

UXViewDriver

UXViewDriver is the seam between the toolkit and a platform. Views, controls, layout and the responder chain above it are one body of code; each platform sits below it. It is also the only platform-specific name a well-written program contains, and that name appears once:

gDriver = new UXAppKitDriver();
#use <UXKit> // or #import "UXViewDriver.xc"

Most readers never implement UXViewDriver; they choose one. This page serves two audiences: someone porting UXKit to a new platform, and someone who wants to understand why the toolkit behaves as it does, since most unexpected behaviour elsewhere follows from something here.

To pick a backend, read the driver model instead.

The protocol falls into eight groups:

groupwhat it covers
windowscreate, open, title, icon, invalidate, destroy, scroll offset
the shadow treestruct* — the flat object array the platform walks
realizationrealizeTree — make native controls and push their state
eventsnextEvent, pumpMessages, runLoop, trackDragStep
drawingbeginViewDraw, treeDraw, setDrawOffset, text measurement
native panelsfile open, colour picker, font picker — each behind a has… flag
menusmenuBuild, menuShow, menuCheck, runPopupMenu
platform servicesclock, settings, directory listing, file operations

The last group exists because a toolkit that can open a window but cannot read a preference forces every app to add its own platform seam, and the app then has two.

bool hasNativeFileOpen(void);
bool hasNativeColorPicker(void);
bool hasNativeFontPicker(void);
bool scrollsNatively(void);
bool driverOwnsRunLoop(void);

Each flag guards a facility the backend may or may not have. Ask, then use the native facility or the toolkit’s own. This is how UXFilePanel is a real NSOpenPanel on macOS and a drawn panel on GEM, from one call site.

driverOwnsRunLoop has the largest effect. iOS and Android return true: their native loops never return from runLoop, so the neutral loop is never entered and the app’s life continues through driver callbacks. Everything else — AppKit (interactive included), GEM, Win32, GTK and the web — returns false and lets the neutral loop pull events with nextEvent. Both shapes exist because each platform requires its own; an interactive AppKit app is the interesting middle case, because the driver does not own the loop and yet nextEvent blocks in [NSApp run] anyway (see the turn hook).

A turn comes from the driver, or from the loop

Section titled “A turn comes from the driver, or from the loop”
bool setTurnHook(turnHook_t* fn, i32 ms)

An animated app asks for a turn with UXApplication.everyTurn, and this is where the answer comes from. The return value says who calls fn:

  • true — the driver armed its own source and calls fn. This is the loop-owning case: interactive AppKit blocks in [NSApp run] inside nextEvent, and iOS and Android never return from runLoop, so nothing above the driver would get a turn at all. AppKit arms a repeating NSTimer on the main queue; Android reposts a Handler message; iOS schedules the same timer.
  • false — the driver has no turn of its own to offer, and the neutral loop paces itself instead: ms becomes the wait it hands nextEvent, so a turn comes round even when no input does, and fn runs after that turn’s draws.

A client that needs to know which it got asks turnIsDriven() rather than guessing. fn takes no arguments and is a plain function, not a bound method: the loop-owning backends hold it as a C function pointer, which has nowhere to keep a captured self. ms of 0 means “every turn the loop has”, and everyTurn(0, 0) stops the clock.

Because the answer is false for a driver that does not own the loop, nextEvent’s timeout is a real deadline:

void nextEvent(i32 timeoutMs, UXEvent* ev) // >0: return UXEventNone on the deadline

A positive timeoutMs means the wait must end on its own — Win32 waits with MsgWaitForMultipleObjects instead of blocking in GetMessageA, GTK attaches a one-shot timer to its GMainContext, GEM adds MU_TIMER to the event mask, the web ring waits on the deadline, and headless AppKit polls for exactly that long. Zero or less keeps the old block-until-there-is-one behaviour, so a client without a clock is unchanged.

GL: the driver owns the surface and the frame

Section titled “GL: the driver owns the surface and the frame”
i32 glKind(void);
bool compositesWithGL(void);
pointer glProc(u8* name);
pointer makeGLContext(pointer view);
void destroyGLContext(pointer view);
void resizeGL(pointer view, i32 w, i32 h);
void presentGL(pointer view);
void glSetSwapInterval(i32 interval);

A view may own a GL context instead of being painted by drawRect. This is the DRI split, not indirect GLX: the driver owns the surface — the native drawable, its order, its resize, its swap — and the app owns the renderer, which loads its own entry points through glProc. Routing GL through UXGraphics instead would be one round trip per call, and is refused.

glKind() names the call set (UX_GL_NONE, UX_GL_GLES3, UX_GL_GL33, UX_GL_WEBGL2); a backend with none answers UX_GL_NONE, and the view is drawn by drawRect like any other. makeGLContext binds a context to a view and presentGL swaps. The driver sets the viewport from the drawable’s own pixels, resizes both in the same turn as the resize, and skips drawRect for a view that owns a context — the two are alternative renderers, never both.

The present cadence is the rule GL rests on: present happens at most once per loop turn, after damage is consolidated, and the driver owns the frame clock. A GL view is the bottom of the stack and every other view is above it, so nothing is ever drawn between two GL draws and the swap is enough on its own. A GL view never runs a clock of its own: it requests a frame (the turn hook) and lets the driver pace it.

compositesWithGL() is the one query about ordering. It asks whether the platform composites the 2-D layer and the GL present in one step, or leaves the driver two producers to order and pace. A view drawn over a GL surface composites on every backend, because the display server composites; the question is who owns that step, and the app picks its overlay and redraw strategy from the answer. AppKit, Win32, GTK and the web answer true (the surface is a distinct plane the compositor merges — an NSOpenGLView subview, a GL child window, a GtkGLArea, a canvas stacked under the 2-D one); GEM, iOS and Android answer false (no GL).

The shadow tree is the platform’s, not ours

Section titled “The shadow tree is the platform’s, not ours”
pointer structNew(void);
i32 structAppend(pointer h, i32 kind, i32 x, i32 y, i32 w, i32 ht);
void structAddChild(pointer h, i32 parent, i32 child);
void structAbsFrame(pointer h, i32 i, i32* x, i32* y, i32* w, i32* ht);

The driver owns the node array. The toolkit holds an opaque handle and never allocates or relinks the array itself. On GEM the array is the AES OBJECT tree, so objc_draw and objc_find work on it directly with no translation.

See UXViewTree for the part the toolkit uses.

Setters push, or they do not — and that is a decision

Section titled “Setters push, or they do not — and that is a decision”

A structSet* either writes the shadow node and pushes to the native control, or writes only the node and leaves the push to the next realizeTree. Both are valid; inconsistency is not.

The rule: anything a user can observe immediately must push immediately. Hidden, enabled, frame and toggle state all push. A flag that only affects the next draw need not.

This affects more than backend authors. A state change that “only works sometimes” is nearly always a setter that writes the shadow tree while something else happens to trigger a realize.

i32 trackDragStep(i32* x, i32* y); // 1 while dragging, 0 on release

Toolkit-drawn drags (a scrollbar thumb, a split divider, an editor moving an object) call this in a loop. It blocks until the pointer moves or the button is released, and emits no events.

This has effects elsewhere. A widget waiting for mouseDragged waits forever. A recorder captures the press and nothing else, so UXEvent has outcome kinds such as UXEventScrolled. A replayed press must not re-enter the loop, which gInputReplay prevents.

A backend whose native controls handle their own dragging returns 0 immediately.

i32 textWidth(u8* s, i32 size);
i32 textWidthStyled(u8* s, u8* family, i32 size, bool bold, bool italic);
i32 textWidthWeight(u8* s, u8* family, i32 size, i32 weight, bool italic);
i32 textAscent(u8* family, i32 size, i32 weight, bool italic);

Only the backend knows its glyphs. UXTextLayout.wrapFont breaks lines using these, while its wrap uses a uniform width and needs no driver. Layout logic is therefore testable headlessly; rendering is not.

textWidthWeight is textWidthStyled with the weight on the CSS scale rather than a bool, so a measure and a drawTextFontRGBA at the same weight agree: textWidthStyled(s, family, size, bold, italic) answers exactly as textWidthWeight at UXWEIGHT_SEMIBOLD or UXWEIGHT_NORMAL. The same string can measure 27 at bold and 25 at 600, and only one of those is what the backend draws.

textAscent is the other half of a measure: the distance from the top of the line — the y a drawText* call is handed — down to the baseline, for a family, size and weight. A caller holding a baseline (a canvas’ y, a print metric, another toolkit’s metric) converts it with y = baseline - driver.textAscent(...). It is the face’s ascent, not the string’s: a line of digits is not shorter than a line with a bracket, and a line whose height moves with its own text is the bug this avoids. It is also the number each backend’s own draw call offsets by, so a conversion done with it lands the ink where the caller asked: every bring-up gate lays one cap-height row and checks the ink’s last row against the baseline the metric names (make mac-weight, make gtk-real, make ios-real, make android-real).

The groups above organise the protocol; this is the surface a backend must provide. All methods are required. There are no optional methods, because a half-implemented backend is worse than an absent one.

A method a platform cannot perform returns a constant, and the capability flags let a caller find out before asking. The contract is that answering is mandatory and doing is not.

i32 windowCreate(i32 x, i32 y, i32 w, i32 h)
void windowSetContent(i32 handle, pointer fn, pointer ud)
void windowOpen(i32 handle, i32 x, i32 y, i32 w, i32 h)
void windowDestroy(i32 handle)
void windowSetTitle(i32 handle, u8* s)
void windowSetSubtitle(i32 handle, u8* s)
void windowSetInfo(i32 handle, u8* s)
void windowSetIcon(i32 handle, u8* slice)
void windowSetModified(i32 handle, bool m)
void windowContentSize(i32 handle, i32 w, i32 h)
i32 windowScrollX(i32 handle)
i32 windowScrollY(i32 handle)
void windowSetScroll(i32 handle, i32 x, i32 y)
void windowContentGeometry(i32 handle, i32* w, i32* h)
void windowInvalidate(i32 handle)
void windowInvalidateRect(i32 handle, i32 x, i32 y, i32 w, i32 h)
void windowOrderFront(i32 handle)
i32 windowAtPoint(i32 x, i32 y)
void treeOffset(pointer tree, i32 obj, i32* ax, i32* ay)
i32 treeHitTest(pointer tree, i32 start, i32 x, i32 y)
void structAbsFrame(pointer h, i32 i, i32* x, i32* y, i32* w, i32* ht)
pointer structNew(void)
void structFree(pointer h)
void structAdopt(pointer h, pointer t, i32 n)
pointer structObjects(pointer h)
i32 structLength(pointer h)
i32 structAppend(pointer h, i32 kind, i32 x, i32 y, i32 w, i32 ht)
void structAddChild(pointer h, i32 parent, i32 child)
void structRemoveChild(pointer h, i32 parent, i32 child)
void structFinalise(pointer h)
void structSetFrame(pointer h, i32 i, i32 x, i32 y, i32 w, i32 ht)
void structFrame(pointer h, i32 i, i32* x, i32* y, i32* w, i32* ht)
void structSetHidden(pointer h, i32 i, i32 on)
i32 structIsHidden(pointer h, i32 i)
void structSetEnabled(pointer h, i32 i, i32 on)
i32 structIsEnabled(pointer h, i32 i)
void structSetSelected(pointer h, i32 i, i32 on)
i32 structIsSelected(pointer h, i32 i)
void structSetClips(pointer h, i32 i, i32 on)
void structSetSpec(pointer h, i32 i, pointer spec)
void structSetSelectable(pointer h, i32 i, i32 on)
void structSetEditable(pointer h, i32 i, i32 on)
void structSetPeer(pointer h, i32 i, pointer peer)
void structSetAutoresize(pointer h, i32 i, i32 mask)
void treeSetUserDraw(pointer fn, pointer ud)
void treeDraw(pointer tree, i32 start, i32 clx, i32 cly, i32 clw, i32 clh)
void realizeTree(i32 handle, pointer tree)
pointer fieldEditorNew(u8* buf, i32 cap)
i32 liveNativeCount(void)
void nextEvent(i32 timeoutMs, UXEvent* ev)
void pumpMessages(i32 timeoutMs, UXEvent* ev)
i32 trackDragStep(i32* x, i32* y)
bool driverOwnsRunLoop(void)
void runLoop(void)
bool setTurnHook(turnHook_t* fn, i32 ms)
UXGraphics* beginViewDraw(i32 ax, i32 ay, i32 aw, i32 ah)
void setDrawOffset(i32 x, i32 y)
bool scrollsNatively(void)
i32 textWidth(u8* s, i32 size)
i32 textWidthStyled(u8* s, u8* family, i32 size, bool bold, bool italic)
i32 textWidthWeight(u8* s, u8* family, i32 size, i32 weight, bool italic)
i32 textAscent(u8* family, i32 size, i32 weight, bool italic)
i32 fontFamilyCount(void)
i32 fontFamilyName(i32 idx, u8* out, i32 cap)
bool hasNativeFileOpen(void)
i32 fileOpen(u8* prompt, u8* startDir, u8* out, i32 outCap)
bool hasNativeColorPicker(void)
i32 pickColor(i32 r, i32 g, i32 b, i32* outR, i32* outG, i32* outB)
bool hasNativeFontPicker(void)
i32 pickFont(u8* inFamily, i32 inSize, i32 inBold, i32 inItalic,
u8* outFamily, i32 outCap, i32* outSize, i32* outBold, i32* outItalic)
i32 fileDelete(u8* path)
i32 fileRename(u8* src, u8* dst)
i32 fileCopy(u8* src, u8* dst)
void nativeScrollTo(pointer h, i32 node, i32 px)
i32 nativeScrollPx(pointer h, i32 node)
i32 alertRun(i32 icon, u8* lines, u8* buttons, i32 defaultBtn)
i32 runPopupMenu(pointer peer, i32 x, i32 y)
pointer menuBuild(pointer defs, i32 n, i32 screenW)
void menuShow(pointer menu, i32 show)
i32 menuItemOrd(pointer menu, i32 titleOrd, i32 itemObj)
void menuCheck(pointer menu, i32 titleOrd, i32 itemOrd, i32 on)
void menuEnable(pointer menu, i32 titleOrd, i32 itemOrd, i32 on)
i32 nowMs(void)
void nowUTC(i32* out7)
i32 localOffsetMinutes(void)
bool settingGet(u8* domain, u8* key, u8* out, i32 cap)
bool settingSet(u8* domain, u8* key, u8* value)
bool settingRemove(u8* domain, u8* key)
i32 formFactorClass(void)
i32 pickColor(i32 r, i32 g, i32 b, i32* outR, i32* outG, i32* outB)
i32 listDir(u8* path, u8* out, i32 outCap)
bool driverAutoresizes(void)
void fieldEditorSetValid(pointer ed, u8* valid)
void fieldEditorSetPlaceholder(pointer ed, u8* s)
void fieldEditorSetSecure(pointer ed, i32 on)
void fieldEditorFree(pointer ed)
i32 editText(pointer tree, i32 obj, i32 key, i32* caret, i32 mode)
bool boot(i32* screenW, i32* screenH)
backenddriver
macOSUXAppKitDriver
WindowsUXWin32Driver
LinuxUXGtkDriver
AtariUXGemDriver
iOSUXIosDriver
AndroidUXAndroidDriver
webUXWebDriver

Each is the one file that knows its platform. Adding an eighth means answering every question here, and the capability flags let a new backend answer “no” to the native panels and still be complete.