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

UXAppKitDriver

UXAppKitDriver is the macOS realization of UXViewDriver. The neutral toolkit (UXView, UXWindow, the widgets) runs on it unchanged.

#use <UXKit> // or #import "UXAppKitDriver.xc"

Like every driver, it is selected at boot and reached through gDriver. Application code names it in one place, the platform line that chooses a backend, and nowhere else.

A program that calls UXAppKitDriver methods directly only runs on macOS.

A UXWindow is a real NSWindow, but a UXView is generally not a real NSView. The driver keeps its own shadow tree, a flat array of nodes with parent and sibling links, and walks it to paint and to hit-test.

Painting goes through one flipped UXDrawView per window, whose drawRect: is the paint entry point. A window with forty views has one native view and forty drawn ones.

UXWin32Driver and UXGtkDriver share this shape. The walk is backend-neutral, and only the painting vocabulary differs.

Some widgets are realized, as native overlays that read directly from the neutral model:

neutralnative
UXTableViewNSTableView
UXOutlineViewNSOutlineView
UXSliderNSSlider
UXPopUpButtonNSPopUpButton
UXStepperNSStepper
UXSegmentedControlNSSegmentedControl
UXProgressBarNSProgressIndicator
UXToolbarNSToolbar (window chrome, not a subview)

The framework’s rule is to use the native UI where the user can tell the difference. A drawn approximation of an NSTableView is never as good as an NSTableView, and a slider that does not feel like the platform’s slider is worse than one that does.

The overlay reads the neutral object and does not copy from it, so the model stays the single source of truth and there is no synchronisation step.

Everything AppKit-specific goes through libUXAppKit.m. At that boundary, no NSRect crosses into xc. The shim’s exported signatures use only primitives:

i32 ux_ak_window_create(i32 x, i32 y, i32 w, i32 h);
void ux_ak_window_open(i32 handle);
i32 ux_ak_open_panel(u8* prompt, u8* startDir, u8* out, i32 outCap);

Windows are i32 handles, not pointers. Strings are u8* with an explicit capacity, and structs do not cross the boundary. Both sides can then agree on the ABI without knowing each other’s layout rules.

Live: events, menus, alerts, scrolling, tables and outlines, and the native open, colour and font panels.

Not implemented:

  • windowSetSubtitle / Info / Icon / Modified. NSWindow has subtitle and documentEdited, so these are unfinished, not unavailable.
  • the toolkit’s own file-panel operations (listDir, fileDelete, …). These are intentionally unused, because macOS presents NSOpenPanel.

When reading the driver, treat an unimplemented method as a gap and an intentionally absent one as a decision.

Headless, capture, and getting a GL surface

Section titled “Headless, capture, and getting a GL surface”

A plain run is headless: it paints offscreen and the app draws its own controls. A headless client that needs the real view tree — GL above all — calls ux_ak_set_capture(1) before boot. Capture mode realises the tree without ever showing a window, which is what the portrait pipeline wants it for; without it realizeTree builds no native view at all, so there is no surface for makeGL to bind and it returns false with a message that reads like a missing GL backend on a backend whose glKind says otherwise.

Two dumps answer “where did the drawing go”, and they are a pair on purpose:

  • ux_ak_gl_grab(peer, path) reads back the GL surface — the map and nothing over it.
  • ux_ak_gl_grab_window(peer, path) reads back the view the tree hangs in — the 2-D views and their text. A panel appears in the window grab and not the surface grab; that asymmetry is the answer, not a bug in either dump.

Which grab a panel’s own drawing lands in follows from one rule, and it is the one thing a GL app has to know:

  • a 2-D paint goes into the parent’s drawRect, and a view’s own paint is drawn before its subviews, so a paint is under the surface;
  • a native view — a control, or a scroll view’s document view — is a real subview, and the surface is added at the bottom of the stack, so a view is over the surface.

A panel over the map is therefore a mix, not one kind of thing. A drawn sheet needs a native subview of its own — UXScrollView’s document is the one the toolkit makes for you, and it draws the peer’s subtree into it — while the buttons and fields standing on that sheet are ordinary native controls and need nothing special. ux_ak_gl_place puts the surface below every sibling for exactly that reason, and neither grab on its own shows the whole window.

Any view can ask for that native subview directly with UXView.setOwnSurface: its kind becomes UXKindSurface, and AppKit realises a real subview at the view’s frame whose drawRect draws the view’s subtree — so the view’s own paint and its children land over the surface, which is the general form of the scroll document. A backend that cannot make one declines and draws the view inline, so one tree is correct everywhere and only the stacking over a GL surface differs.

Under [NSApp run], AppKit owns the thread, and the toolkit’s dispatch is driven from it (see UXApplication).

Note the nuance: driverOwnsRunLoop returns false for AppKit, because the neutral loop is still the one that runs — its nextEvent simply blocks in [NSApp run] while the platform’s loop dispatches. The consequence is that nothing above the driver gets a turn, which is why an interactive app’s frame clock comes from the driver’s own timer (setTurnHook answers true here, and false in a headless run).

For the same reason, an @autoreleasepool around window ordering or activation causes trouble: the scope ends inside AppKit’s own bookkeeping.