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"You implement this only to add a backend
Section titled “You implement this only to add a backend”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 eight groups
Section titled “The eight groups”The protocol falls into eight groups:
| group | what it covers |
|---|---|
| windows | create, open, title, icon, invalidate, destroy, scroll offset |
| the shadow tree | struct* — the flat object array the platform walks |
| realization | realizeTree — make native controls and push their state |
| events | nextEvent, pumpMessages, runLoop, trackDragStep |
| drawing | beginViewDraw, treeDraw, setDrawOffset, text measurement |
| native panels | file open, colour picker, font picker — each behind a has… flag |
| menus | menuBuild, menuShow, menuCheck, runPopupMenu |
| platform services | clock, 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.
The patterns worth knowing
Section titled “The patterns worth knowing”Capability flags, not assumptions
Section titled “Capability flags, not assumptions”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]insidenextEvent, and iOS and Android never return fromrunLoop, so nothing above the driver would get a turn at all. AppKit arms a repeatingNSTimeron the main queue; Android reposts aHandlermessage; iOS schedules the same timer. - false — the driver has no turn of its own to offer, and the neutral
loop paces itself instead:
msbecomes the wait it handsnextEvent, so a turn comes round even when no input does, andfnruns 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 deadlineA 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.
A drag is modal
Section titled “A drag is modal”i32 trackDragStep(i32* x, i32* y); // 1 while dragging, 0 on releaseToolkit-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.
Text measurement belongs to the backend
Section titled “Text measurement belongs to the backend”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 whole protocol
Section titled “The whole protocol”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.
Windows
Section titled “Windows”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)The shadow tree
Section titled “The shadow tree”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)Realization
Section titled “Realization”void realizeTree(i32 handle, pointer tree)pointer fieldEditorNew(u8* buf, i32 cap)i32 liveNativeCount(void)Events and the run loop
Section titled “Events and the run loop”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)Drawing
Section titled “Drawing”UXGraphics* beginViewDraw(i32 ax, i32 ay, i32 aw, i32 ah)void setDrawOffset(i32 x, i32 y)bool scrollsNatively(void)Text measurement
Section titled “Text measurement”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)Native panels
Section titled “Native panels”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)Platform services
Section titled “Platform services”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)Everything else
Section titled “Everything else”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)Implementations
Section titled “Implementations”| backend | driver |
|---|---|
| macOS | UXAppKitDriver |
| Windows | UXWin32Driver |
| Linux | UXGtkDriver |
| Atari | UXGemDriver |
| iOS | UXIosDriver |
| Android | UXAndroidDriver |
| web | UXWebDriver |
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.
See also
Section titled “See also”- The driver model and multiplatform:
choosing one, and writing the
#ifdefseam correctly UXViewTree: the shadow tree from the toolkit’s sideUXGraphics: the drawing protocol a driver supplies a realization ofUXEvent: whatnextEventproduces