std1.0.0-beta-7 SafeC docs

Source gui/gui_cocoa.sc

View in the repository

// SafeC Standard Library — GUI backend: macOS (Cocoa via the Objective-C
// runtime + Core Graphics). The struct GuiBackend for AppKit (see
// gui_backend.h for the contract it implements and gui.h for the portable
// API on top).
// safeguard: link macos -framework Cocoa -lobjc
#pragma once
#include <std/stddef.h>
//
// There is no Objective-C support in safec itself — every AppKit/Core
// Graphics call here goes through the plain-C Objective-C runtime API
// (objc_msgSend et al., <objc/message.h>/<objc/runtime.h>) and Core
// Graphics' own C API, the same technique minimal Cocoa bindings in other
// languages without a real ObjC bridge use. 'objc_msgSend' is declared
// once with a generic two-pointer-argument signature and then reinterpret-
// cast (via 'typedef fn ... Name;' function-pointer types, one per call
// shape: receiver and selector, then the message's arguments) to whatever
// concrete signature a given message send actually needs before calling
// through it — the real ABI dispatch depends entirely on the argument/
// return types at the call site, not on how the symbol itself is declared.
// Messages returning an NSRect (32 bytes) go through objc_msgSend_stret on
// x86_64, as the ABI requires there; arm64 has no _stret variant.
//
// Rendering model: a window's content view is layer-backed
// (setWantsLayer:YES); each present copies the RGBA buffer (premultiplied,
// kCGImageAlphaPremultipliedLast — exactly our format, no conversion) into
// a CGImage and sets it as the CALayer's 'contents', which AppKit
// composites to the screen on the next display pass. hiDpi windows get a
// buffer of the content size times backingScaleFactor and set the layer's
// contentsScale to match, so Retina displays show it 1:1.
//
// Events: pump() dequeues NSEvents itself (nextEventMatchingMask:...) and
// routes each to the GuiWindowState of [event window]; everything but key
// events is also passed on to [NSApp sendEvent:] so the title bar, resize
// handles and window buttons keep working (key events aren't, so AppKit
// doesn't beep about keys no responder handles). Per-window close, resize,
// move, focus and backing-scale changes come from a delegate object per
// window (class SafeCWindowDelegate, registered at first use with
// objc_allocateClassPair/class_addMethod) — windowShouldClose: answers NO
// and posts GUI_EVENT_CLOSE, so closing is the application's decision.
// Mouse enter/leave are derived from the pointer position on mouse moves.

#include <std/gui/gui.h>
#include <std/gui/gui_backend.h>
#include <std/gui/gui_headless.h>
#include <std/gui/gui_draw.h>
#include <std/mem.h>
#include <std/time.h>
#include <std/collections/string.h>

namespace std {

// Opaque Objective-C runtime types: an object ('id'; a Class is one too,
// and is messaged as one), a selector (SEL), an autorelease pool token.
// A method implementation (IMP) is registered untyped and cast from its
// real signature (the receiver, the selector, then the arguments).
struct ObjcId_   { char opaque_; };
struct ObjcSel_  { char opaque_; };
struct ObjcPool_ { char opaque_; };
typedef fn void() ObjcImp_;
// Core Graphics' opaque types.
struct CGColorSpace_  { char opaque_; };
struct CGDataProvider_ { char opaque_; };
struct CGImage_       { char opaque_; };
struct CGContext_     { char opaque_; };
// CGDataProviderReleaseDataCallback: 'info' and 'data' are the copied bytes.
typedef fn void(unsigned char*, const unsigned char*, unsigned long) CGReleaseData_;

extern ObjcId_*  objc_getClass(const char* name);
extern ObjcSel_* sel_registerName(const char* name);
extern ObjcId_*  objc_msgSend(ObjcId_* recv, ObjcSel_* op);
#if defined(__x86_64__)
extern void      objc_msgSend_stret(ObjcId_* recv, ObjcSel_* op);
#endif
extern ObjcId_*  objc_allocateClassPair(ObjcId_* superclass, const char* name, unsigned long extraBytes);
extern void      objc_registerClassPair(ObjcId_* cls);
extern int       class_addMethod(ObjcId_* cls, ObjcSel_* sel, ObjcImp_ imp, const char* types);
extern ObjcPool_* objc_autoreleasePoolPush();
extern void      objc_autoreleasePoolPop(ObjcPool_* pool);

extern CGColorSpace_* CGColorSpaceCreateDeviceRGB();
extern CGDataProvider_* CGDataProviderCreateWithData(unsigned char* info, const unsigned char* data, unsigned long size,
                                                     CGReleaseData_ releaseFunc);
extern CGImage_* CGImageCreate(unsigned long width, unsigned long height, unsigned long bitsPerComponent,
                               unsigned long bitsPerPixel, unsigned long bytesPerRow, CGColorSpace_* space,
                               unsigned int bitmapInfo, CGDataProvider_* provider, const double* decode,
                               int shouldInterpolate, int intent);
extern void  CGImageRelease(CGImage_* image);
extern void  CGColorSpaceRelease(CGColorSpace_* space);
extern void  CGDataProviderRelease(CGDataProvider_* provider);

struct NSRect { double x; double y; double w; double h; };
struct NSPoint { double x; double y; };
struct NSSize { double w; double h; };

typedef fn ObjcId_*(ObjcId_*, ObjcSel_*) MsgSend0;
typedef fn ObjcId_*(ObjcId_*, ObjcSel_*, ObjcId_*) MsgSend1;
typedef fn ObjcId_*(ObjcId_*, ObjcSel_*, unsigned long) MsgSendULong;
typedef fn ObjcId_*(ObjcId_*, ObjcSel_*, long) MsgSendLong;
typedef fn ObjcId_*(ObjcId_*, ObjcSel_*, int) MsgSendInt;
typedef fn ObjcId_*(ObjcId_*, ObjcSel_*, double) MsgSendDoubleArg;
typedef fn ObjcId_*(ObjcId_*, ObjcSel_*, struct NSRect, unsigned long, unsigned long, int) MsgSendInitRect;
typedef fn void(ObjcId_*, ObjcSel_*, struct NSSize) MsgSendSize;
typedef fn void(ObjcId_*, ObjcSel_*, struct NSPoint) MsgSendPointArg;
typedef fn struct NSPoint(ObjcId_*, ObjcSel_*) MsgSendPoint;
typedef fn struct NSRect(ObjcId_*, ObjcSel_*) MsgSendRect;
typedef fn struct NSRect(ObjcId_*, ObjcSel_*, struct NSRect) MsgSendRectRect;
typedef fn unsigned long(ObjcId_*, ObjcSel_*) MsgSendRetULong;
typedef fn long(ObjcId_*, ObjcSel_*) MsgSendRetLong;
typedef fn unsigned short(ObjcId_*, ObjcSel_*) MsgSendRetUShort;
typedef fn signed char(ObjcId_*, ObjcSel_*) MsgSendRetBool;
typedef fn void(ObjcId_*, ObjcSel_*, ObjcId_*) MsgSendVoid1;
typedef fn ObjcId_*(ObjcId_*, ObjcSel_*, const char*) MsgSendStr;
typedef fn const char*(ObjcId_*, ObjcSel_*) MsgSendRetStr;
typedef fn void(ObjcId_*, ObjcSel_*) MsgSendVoid0;
typedef fn void(ObjcId_*, ObjcSel_*, ObjcId_*, int) MsgSendVoid1Int;
typedef fn signed char(ObjcId_*, ObjcSel_*, ObjcId_*, ObjcId_*) MsgSendBool2;
typedef fn ObjcId_*(ObjcId_*, ObjcSel_*, unsigned long, ObjcId_*, ObjcId_*, int) MsgSendNextEvent;
typedef fn double(ObjcId_*, ObjcSel_*) MsgSendDouble;
typedef fn ObjcId_*(ObjcId_*, ObjcSel_*, unsigned long, struct NSPoint, unsigned long, double, long, ObjcId_*, short, long, long) MsgSendOtherEvent;

// ── one-time class/selector/app setup ───────────────────────────────────────

struct GuiCocoaWindow {
    ObjcId_* window;       // NSWindow*
    ObjcId_* view;         // its content view
    ObjcId_* delegate;     // SafeCWindowDelegate instance
};

static int   gInited = 0;
static ObjcId_* gNSApp;
static ObjcId_* gDelegateClass;
static ObjcId_* gDefaultMode;
static ObjcId_* gPasteType;
static int   gCursorHidden = 0;
static struct GuiBackend gCocoaBackend;
static int gCocoaBackendInit = 0;

static ObjcSel_* __gui_cocoa_sel(const char* name) {
    unsafe { return sel_registerName(name); }
}

static ObjcId_* __gui_cocoa_str(const char* utf8) {
    unsafe {
        MsgSendStr strMsg = (MsgSendStr)objc_msgSend;
        return strMsg(objc_getClass("NSString"), __gui_cocoa_sel("stringWithUTF8String:"), utf8);
    }
}

static struct NSRect __gui_cocoa_rect(ObjcId_* obj, const char* sel) {
    unsafe {
#if defined(__x86_64__)
        MsgSendRect m = (MsgSendRect)objc_msgSend_stret;
#else
        MsgSendRect m = (MsgSendRect)objc_msgSend;
#endif
        return m(obj, __gui_cocoa_sel(sel));
    }
}

static double __gui_cocoa_screen_height() {
    unsafe {
        MsgSend0 m = (MsgSend0)objc_msgSend;
        ObjcId_* screen = m(objc_getClass("NSScreen"), __gui_cocoa_sel("mainScreen"));
        if (screen == null) { return 0.0; }
        struct NSRect r = __gui_cocoa_rect(screen, "frame");
        return r.h;
    }
}

static ?&GuiWindowState __gui_cocoa_find(ObjcId_* nsWindow) {
    unsafe { return gui_backend_find((unsigned long long)(unsigned long)nsWindow); }
}

static ObjcId_* __gui_cocoa_notif_window(ObjcId_* notif) {
    unsafe {
        MsgSend0 m = (MsgSend0)objc_msgSend;
        return m(notif, __gui_cocoa_sel("object"));
    }
}

// The content area's size in points and its top-left on screen (y down).
static void __gui_cocoa_update_geometry(struct GuiWindowState* st, int postEvents) {
    unsafe {
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st->native;
        if (w == (struct GuiCocoaWindow*)0) { return; }
        struct NSRect cv = __gui_cocoa_rect(w->view, "frame");
        struct NSRect fr = __gui_cocoa_rect(w->window, "frame");
#if defined(__x86_64__)
        MsgSendRectRect toContent = (MsgSendRectRect)objc_msgSend_stret;
#else
        MsgSendRectRect toContent = (MsgSendRectRect)objc_msgSend;
#endif
        struct NSRect content = toContent(w->window, __gui_cocoa_sel("contentRectForFrameRect:"), fr);
        int lw = (int)(cv.w + 0.5);
        int lh = (int)(cv.h + 0.5);
        int px = (int)((double)lw * st->scale + 0.5);
        int py = (int)((double)lh * st->scale + 0.5);
        if (lw > 0 && lh > 0 && (px != st->pixelW || py != st->pixelH || lw != st->logicalW || lh != st->logicalH)) {
            st->logicalW = lw;
            st->logicalH = lh;
            st->pixelW = px;
            st->pixelH = py;
            if (postEvents) {
                struct GuiEvent ev = GuiEvent::new(GUI_EVENT_RESIZE);
                ev.width = px;
                ev.height = py;
                gui_backend_post(st, ev);
            }
        }
        double sh = __gui_cocoa_screen_height();
        int nx = (int)content.x;
        int ny = (int)(sh - (content.y + content.h));
        if (nx != st->x || ny != st->y) {
            st->x = nx;
            st->y = ny;
            if (postEvents) {
                struct GuiEvent mv = GuiEvent::new(GUI_EVENT_MOVE);
                mv.x = (double)nx;
                mv.y = (double)ny;
                gui_backend_post(st, mv);
            }
        }
    }
}

signed char __gui_cocoa_window_should_close(ObjcId_* recv, ObjcSel_* cmd, ObjcId_* sender) {
    ?&GuiWindowState ost = __gui_cocoa_find(sender);
    unsafe {
        if (!ost.is_null()) { gui_backend_post((struct GuiWindowState*)ost, GuiEvent::new(GUI_EVENT_CLOSE)); }
    }
    return (signed char)0;   // NO: the application closes it (GuiWindow::destroy)
}

void __gui_cocoa_window_did_resize(ObjcId_* recv, ObjcSel_* cmd, ObjcId_* notif) {
    ?&GuiWindowState ost = __gui_cocoa_find(__gui_cocoa_notif_window(notif));
    unsafe { if (!ost.is_null()) { __gui_cocoa_update_geometry((struct GuiWindowState*)ost, 1); } }
}

void __gui_cocoa_window_did_move(ObjcId_* recv, ObjcSel_* cmd, ObjcId_* notif) {
    ?&GuiWindowState ost = __gui_cocoa_find(__gui_cocoa_notif_window(notif));
    unsafe { if (!ost.is_null()) { __gui_cocoa_update_geometry((struct GuiWindowState*)ost, 1); } }
}

void __gui_cocoa_window_did_become_key(ObjcId_* recv, ObjcSel_* cmd, ObjcId_* notif) {
    ?&GuiWindowState ost = __gui_cocoa_find(__gui_cocoa_notif_window(notif));
    unsafe {
        if (ost.is_null()) { return; }
        struct GuiWindowState* st = (struct GuiWindowState*)ost;
        if (!st->focused) {
            st->focused = 1;
            gui_backend_post(st, GuiEvent::new(GUI_EVENT_FOCUS_IN));
        }
    }
}

void __gui_cocoa_window_did_resign_key(ObjcId_* recv, ObjcSel_* cmd, ObjcId_* notif) {
    ?&GuiWindowState ost = __gui_cocoa_find(__gui_cocoa_notif_window(notif));
    unsafe {
        if (ost.is_null()) { return; }
        struct GuiWindowState* st = (struct GuiWindowState*)ost;
        if (st->focused) {
            st->focused = 0;
            gui_backend_post(st, GuiEvent::new(GUI_EVENT_FOCUS_OUT));
        }
    }
}

void __gui_cocoa_window_did_change_backing(ObjcId_* recv, ObjcSel_* cmd, ObjcId_* notif) {
    ?&GuiWindowState ost = __gui_cocoa_find(__gui_cocoa_notif_window(notif));
    unsafe {
        if (ost.is_null()) { return; }
        struct GuiWindowState* st = (struct GuiWindowState*)ost;
        if (!st->hiDpi) { return; }
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st->native;
        MsgSendDouble scaleMsg = (MsgSendDouble)objc_msgSend;
        double s = scaleMsg(w->window, __gui_cocoa_sel("backingScaleFactor"));
        if (s <= 0.0 || s == st->scale) { return; }
        st->scale = s;
        st->pixelW = (int)((double)st->logicalW * s + 0.5);
        st->pixelH = (int)((double)st->logicalH * s + 0.5);
        struct GuiEvent ev = GuiEvent::new(GUI_EVENT_SCALE);
        ev.width = st->pixelW;
        ev.height = st->pixelH;
        gui_backend_post(st, ev);
    }
}

static int __gui_cocoa_init() {
    if (gInited) { return 1; }
    unsafe {
        ObjcId_* appCls = objc_getClass("NSApplication");
        if (appCls == null) { return 0; }
        MsgSend0 sharedApp = (MsgSend0)objc_msgSend;
        gNSApp = sharedApp(appCls, __gui_cocoa_sel("sharedApplication"));

        // NSApplicationActivationPolicyRegular = 0
        MsgSendLong setPolicy = (MsgSendLong)objc_msgSend;
        setPolicy(gNSApp, __gui_cocoa_sel("setActivationPolicy:"), 0L);

        MsgSendVoid0 finishLaunching = (MsgSendVoid0)objc_msgSend;
        finishLaunching(gNSApp, __gui_cocoa_sel("finishLaunching"));

        MsgSendInt activate = (MsgSendInt)objc_msgSend;
        activate(gNSApp, __gui_cocoa_sel("activateIgnoringOtherApps:"), 1);

        ObjcId_* nsObjectCls = objc_getClass("NSObject");
        gDelegateClass = objc_allocateClassPair(nsObjectCls, "SafeCWindowDelegate", 0UL);
        if (gDelegateClass != null) {
            class_addMethod(gDelegateClass, __gui_cocoa_sel("windowShouldClose:"),
                            (ObjcImp_)__gui_cocoa_window_should_close, "c@:@");
            class_addMethod(gDelegateClass, __gui_cocoa_sel("windowDidResize:"),
                            (ObjcImp_)__gui_cocoa_window_did_resize, "v@:@");
            class_addMethod(gDelegateClass, __gui_cocoa_sel("windowDidMove:"),
                            (ObjcImp_)__gui_cocoa_window_did_move, "v@:@");
            class_addMethod(gDelegateClass, __gui_cocoa_sel("windowDidBecomeKey:"),
                            (ObjcImp_)__gui_cocoa_window_did_become_key, "v@:@");
            class_addMethod(gDelegateClass, __gui_cocoa_sel("windowDidResignKey:"),
                            (ObjcImp_)__gui_cocoa_window_did_resign_key, "v@:@");
            class_addMethod(gDelegateClass, __gui_cocoa_sel("windowDidChangeBackingProperties:"),
                            (ObjcImp_)__gui_cocoa_window_did_change_backing, "v@:@");
            objc_registerClassPair(gDelegateClass);
        } else {
            gDelegateClass = objc_getClass("SafeCWindowDelegate");   // registered earlier in this process
        }
        // [NSString stringWithUTF8String:] returns autoreleased objects;
        // keep these two alive for the process.
        MsgSend0 retainMsg = (MsgSend0)objc_msgSend;
        gDefaultMode = retainMsg(__gui_cocoa_str("kCFRunLoopDefaultMode"), __gui_cocoa_sel("retain"));
        gPasteType = retainMsg(__gui_cocoa_str("public.utf8-plain-text"), __gui_cocoa_sel("retain"));
    }
    gInited = 1;
    return 1;
}

// ── window creation ──────────────────────────────────────────────────────────

static void __gui_cocoa_apply_limits(&GuiWindowState st) {
    unsafe {
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st.native;
        if (w == (struct GuiCocoaWindow*)0) { return; }
        MsgSendSize setSize = (MsgSendSize)objc_msgSend;
        struct NSSize mn;
        mn.w = (double)(st.minW > 0 ? st.minW : 1);
        mn.h = (double)(st.minH > 0 ? st.minH : 1);
        setSize(w->window, __gui_cocoa_sel("setContentMinSize:"), mn);
        struct NSSize mx;
        mx.w = st.maxW > 0 ? (double)st.maxW : 100000.0;
        mx.h = st.maxH > 0 ? (double)st.maxH : 100000.0;
        setSize(w->window, __gui_cocoa_sel("setContentMaxSize:"), mx);
    }
}

static int __gui_cocoa_create(&GuiWindowState st, const &GuiWindowOptions opts) {
    unsafe {
        ObjcPool_* pool = objc_autoreleasePoolPush();
        ObjcId_* winCls = objc_getClass("NSWindow");
        MsgSend0 allocMsg = (MsgSend0)objc_msgSend;
        ObjcId_* winAlloc = allocMsg(winCls, __gui_cocoa_sel("alloc"));

        double sh = __gui_cocoa_screen_height();
        struct NSRect rect;
        rect.w = (double)st.logicalW;
        rect.h = (double)st.logicalH;
        rect.x = st.x != GUI_POS_DEFAULT ? (double)st.x : 100.0;
        rect.y = st.y != GUI_POS_DEFAULT ? sh - (double)st.y - rect.h : 100.0;
        // styleMask: titled(1) | closable(2) | miniaturizable(4) | resizable(8); borderless 0
        unsigned long style = 0UL;
        if (opts.decorated) {
            style = 1UL | 2UL | 4UL;
            if (opts.resizable) { style = style | 8UL; }
        }
        MsgSendInitRect initMsg = (MsgSendInitRect)objc_msgSend;
        ObjcId_* nsWin = initMsg(winAlloc, __gui_cocoa_sel("initWithContentRect:styleMask:backing:defer:"),
                              rect, style, 2UL, 0);
        if (nsWin == null) { objc_autoreleasePoolPop(pool); return 0; }

        // We own the window (GuiWindow::destroy releases it); AppKit must
        // not release it again when it closes.
        MsgSendInt setReleased = (MsgSendInt)objc_msgSend;
        setReleased(nsWin, __gui_cocoa_sel("setReleasedWhenClosed:"), 0);

        MsgSendVoid1 setTitle = (MsgSendVoid1)objc_msgSend;
        setTitle(nsWin, __gui_cocoa_sel("setTitle:"), __gui_cocoa_str((const char*)st.title.data));

        if (st.x == GUI_POS_DEFAULT || st.y == GUI_POS_DEFAULT) {
            MsgSendVoid0 centerMsg = (MsgSendVoid0)objc_msgSend;
            centerMsg(nsWin, __gui_cocoa_sel("center"));
        }
        MsgSendInt acceptMoves = (MsgSendInt)objc_msgSend;
        acceptMoves(nsWin, __gui_cocoa_sel("setAcceptsMouseMovedEvents:"), 1);
        if (opts.topmost) {
            MsgSendLong setLevel = (MsgSendLong)objc_msgSend;
            setLevel(nsWin, __gui_cocoa_sel("setLevel:"), 3L);          // NSFloatingWindowLevel
        }

        // Delegate (close/resize/move/focus/scale).
        MsgSend0 delAlloc = (MsgSend0)objc_msgSend;
        ObjcId_* delInstance = delAlloc(gDelegateClass, __gui_cocoa_sel("alloc"));
        MsgSend0 delInit = (MsgSend0)objc_msgSend;
        delInstance = delInit(delInstance, __gui_cocoa_sel("init"));
        MsgSendVoid1 setDelegate = (MsgSendVoid1)objc_msgSend;
        setDelegate(nsWin, __gui_cocoa_sel("setDelegate:"), delInstance);

        // Layer-backed content view, for CGImage-based presentation.
        MsgSend0 contentView = (MsgSend0)objc_msgSend;
        ObjcId_* view = contentView(nsWin, __gui_cocoa_sel("contentView"));
        MsgSendInt setWantsLayer = (MsgSendInt)objc_msgSend;
        setWantsLayer(view, __gui_cocoa_sel("setWantsLayer:"), 1);

        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)alloc_zeroed_raw_(sizeof(struct GuiCocoaWindow));
        w->window = nsWin;
        w->view = view;
        w->delegate = delInstance;
        st.native = (?&Erased)w;
        st.key = (unsigned long long)(unsigned long)nsWin;

        st.scale = 1.0;
        if (opts.hiDpi) {
            MsgSendDouble scaleMsg = (MsgSendDouble)objc_msgSend;
            double s = scaleMsg(nsWin, __gui_cocoa_sel("backingScaleFactor"));
            if (s > 0.0) { st.scale = s; }
        }
        st.pixelW = (int)((double)st.logicalW * st.scale + 0.5);
        st.pixelH = (int)((double)st.logicalH * st.scale + 0.5);
        MsgSend0 layerMsg = (MsgSend0)objc_msgSend;
        ObjcId_* layer = layerMsg(view, __gui_cocoa_sel("layer"));
        if (layer != null) {
            MsgSendDoubleArg setScale = (MsgSendDoubleArg)objc_msgSend;
            setScale(layer, __gui_cocoa_sel("setContentsScale:"), st.scale);
        }
        __gui_cocoa_apply_limits(st);
        if (!opts.resizable) {
            struct NSSize fixed;
            fixed.w = (double)st.logicalW;
            fixed.h = (double)st.logicalH;
            MsgSendSize setSize = (MsgSendSize)objc_msgSend;
            setSize(nsWin, __gui_cocoa_sel("setContentMinSize:"), fixed);
            setSize(nsWin, __gui_cocoa_sel("setContentMaxSize:"), fixed);
        }
        __gui_cocoa_update_geometry((struct GuiWindowState*)st, 0);

        if (opts.visible) {
            MsgSendVoid1 orderFront = (MsgSendVoid1)objc_msgSend;
            orderFront(nsWin, __gui_cocoa_sel("makeKeyAndOrderFront:"), null);
        }
        objc_autoreleasePoolPop(pool);
        return 1;
    }
}

static void __gui_cocoa_destroy(&GuiWindowState st) {
    unsafe {
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st.native;
        if (w == (struct GuiCocoaWindow*)0) { return; }
        MsgSendVoid1 setDelegate = (MsgSendVoid1)objc_msgSend;
        setDelegate(w->window, __gui_cocoa_sel("setDelegate:"), null);
        MsgSendVoid0 closeMsg = (MsgSendVoid0)objc_msgSend;
        closeMsg(w->window, __gui_cocoa_sel("close"));
        MsgSendVoid0 releaseMsg = (MsgSendVoid0)objc_msgSend;
        releaseMsg(w->window, __gui_cocoa_sel("release"));
        releaseMsg(w->delegate, __gui_cocoa_sel("release"));
        dealloc(w);
        st.native = null;
    }
}

// ── presentation ─────────────────────────────────────────────────────────────

// CGDataProviderReleaseDataCallback: frees the copy made for one present.
void __gui_cocoa_release_pixels(unsigned char* info, const unsigned char* data, unsigned long size) {
    unsafe { dealloc(info); }
}

static void __gui_cocoa_present(&GuiWindowState st, const &unsigned char px, int bw, int bh,
                                int x, int y, int rw, int rh) {
    unsafe {
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st.native;
        if (w == (struct GuiCocoaWindow*)0) { return; }
        ObjcPool_* pool = objc_autoreleasePoolPush();
        MsgSend0 layerMsg = (MsgSend0)objc_msgSend;
        ObjcId_* layer = layerMsg(w->view, __gui_cocoa_sel("layer"));
        if (layer == null) { objc_autoreleasePoolPop(pool); return; }

        // The layer keeps reading its contents after this returns, and the
        // window's buffer is reallocated on resize: hand it a copy, freed
        // by the data provider's release callback.
        unsigned long rowBytes = (unsigned long)bw * 4UL;
        unsigned long total = rowBytes * (unsigned long)bh;
        unsigned char* copy = (unsigned char*)alloc_raw_(total);
        if (copy == null) { objc_autoreleasePoolPop(pool); return; }
        safe_memcpy(copy[0..total], ((const unsigned char*)px)[0..total]);
        CGColorSpace_* space = CGColorSpaceCreateDeviceRGB();
        CGDataProvider_* provider = CGDataProviderCreateWithData(copy, (const unsigned char*)copy, total,
                                                                 __gui_cocoa_release_pixels);
        // bitmapInfo = kCGImageAlphaPremultipliedLast (1)
        CGImage_* image = CGImageCreate((unsigned long)bw, (unsigned long)bh, 8UL, 32UL,
                                    rowBytes, space, 1U, provider, (const double*)0, 0, 0);

        MsgSendVoid1 setContents = (MsgSendVoid1)objc_msgSend;
        setContents(layer, __gui_cocoa_sel("setContents:"), (ObjcId_*)image);   // a CGImageRef is a CF object

        CGImageRelease(image);
        CGDataProviderRelease(provider);
        CGColorSpaceRelease(space);
        objc_autoreleasePoolPop(pool);
    }
}

// ── window management ───────────────────────────────────────────────────────

static void __gui_cocoa_set_title(&GuiWindowState st, const &char title) {
    unsafe {
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st.native;
        if (w == (struct GuiCocoaWindow*)0) { return; }
        ObjcPool_* pool = objc_autoreleasePoolPush();
        MsgSendVoid1 setTitle = (MsgSendVoid1)objc_msgSend;
        setTitle(w->window, __gui_cocoa_sel("setTitle:"), __gui_cocoa_str(title));
        objc_autoreleasePoolPop(pool);
    }
}

static void __gui_cocoa_set_size(&GuiWindowState st, int lw, int lh) {
    unsafe {
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st.native;
        if (w == (struct GuiCocoaWindow*)0) { return; }
        struct NSSize s;
        s.w = (double)lw;
        s.h = (double)lh;
        MsgSendSize setSize = (MsgSendSize)objc_msgSend;
        setSize(w->window, __gui_cocoa_sel("setContentSize:"), s);   // windowDidResize: reports it
    }
}

static void __gui_cocoa_set_position(&GuiWindowState st, int x, int y) {
    unsafe {
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st.native;
        if (w == (struct GuiCocoaWindow*)0) { return; }
        // Places the frame's top-left corner (the title bar's, for a titled
        // window): AppKit coordinates have y up from the bottom of the main
        // screen.
        struct NSPoint p;
        p.x = (double)x;
        p.y = __gui_cocoa_screen_height() - (double)y;
        MsgSendPointArg setTop = (MsgSendPointArg)objc_msgSend;
        setTop(w->window, __gui_cocoa_sel("setFrameTopLeftPoint:"), p);
    }
}

static void __gui_cocoa_set_visible(&GuiWindowState st, int v) {
    unsafe {
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st.native;
        if (w == (struct GuiCocoaWindow*)0) { return; }
        MsgSendVoid1 m = (MsgSendVoid1)objc_msgSend;
        m(w->window, __gui_cocoa_sel(v ? "orderFront:" : "orderOut:"), null);
    }
}

static void __gui_cocoa_focus(&GuiWindowState st) {
    unsafe {
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st.native;
        if (w == (struct GuiCocoaWindow*)0) { return; }
        MsgSendInt activate = (MsgSendInt)objc_msgSend;
        activate(gNSApp, __gui_cocoa_sel("activateIgnoringOtherApps:"), 1);
        MsgSendVoid1 orderFront = (MsgSendVoid1)objc_msgSend;
        orderFront(w->window, __gui_cocoa_sel("makeKeyAndOrderFront:"), null);
    }
}

static void __gui_cocoa_apply_cursor(int c) {
    unsafe {
        MsgSend0 m = (MsgSend0)objc_msgSend;
        ObjcId_* cls = objc_getClass("NSCursor");
        if (c == GUI_CURSOR_HIDDEN) {
            if (!gCursorHidden) {
                MsgSendVoid0 hide = (MsgSendVoid0)objc_msgSend;
                hide(cls, __gui_cocoa_sel("hide"));
                gCursorHidden = 1;
            }
            return;
        }
        if (gCursorHidden) {
            MsgSendVoid0 unhide = (MsgSendVoid0)objc_msgSend;
            unhide(cls, __gui_cocoa_sel("unhide"));
            gCursorHidden = 0;
        }
        const char* name = "arrowCursor";
        if (c == GUI_CURSOR_IBEAM) { name = "IBeamCursor"; }
        else if (c == GUI_CURSOR_HAND) { name = "pointingHandCursor"; }
        else if (c == GUI_CURSOR_CROSSHAIR) { name = "crosshairCursor"; }
        else if (c == GUI_CURSOR_RESIZE_EW) { name = "resizeLeftRightCursor"; }
        else if (c == GUI_CURSOR_RESIZE_NS) { name = "resizeUpDownCursor"; }
        else if (c == GUI_CURSOR_MOVE) { name = "openHandCursor"; }
        else if (c == GUI_CURSOR_NOT_ALLOWED) { name = "operationNotAllowedCursor"; }
        ObjcId_* cur = m(cls, __gui_cocoa_sel(name));
        if (cur != null) {
            MsgSendVoid0 setMsg = (MsgSendVoid0)objc_msgSend;
            setMsg(cur, __gui_cocoa_sel("set"));
        }
    }
}

static void __gui_cocoa_set_cursor(&GuiWindowState st, int c) {
    if (st.hovered) { __gui_cocoa_apply_cursor(c); }
}

// ── event loop ────────────────────────────────────────────────────────────────

static int __gui_cocoa_keycode(unsigned short native) {
    int n = (int)native;
    if (n == 123) { return GUI_KEY_LEFT; }
    if (n == 124) { return GUI_KEY_RIGHT; }
    if (n == 126) { return GUI_KEY_UP; }
    if (n == 125) { return GUI_KEY_DOWN; }
    if (n == 36 || n == 76) { return GUI_KEY_ENTER; }
    if (n == 53) { return GUI_KEY_ESCAPE; }
    if (n == 48) { return GUI_KEY_TAB; }
    if (n == 51) { return GUI_KEY_BACKSPACE; }
    if (n == 49) { return GUI_KEY_SPACE; }
    if (n == 117) { return GUI_KEY_DELETE; }
    if (n == 115) { return GUI_KEY_HOME; }
    if (n == 119) { return GUI_KEY_END; }
    if (n == 116) { return GUI_KEY_PAGE_UP; }
    if (n == 121) { return GUI_KEY_PAGE_DOWN; }
    if (n == 114) { return GUI_KEY_INSERT; }
    if (n == 56 || n == 60) { return GUI_KEY_SHIFT; }
    if (n == 59 || n == 62) { return GUI_KEY_CONTROL; }
    if (n == 58 || n == 61) { return GUI_KEY_ALT; }
    if (n == 55 || n == 54) { return GUI_KEY_META; }
    if (n == 110) { return GUI_KEY_MENU; }
    // F1..F12
    if (n == 122) { return GUI_KEY_F1; }
    if (n == 120) { return GUI_KEY_F1 + 1; }
    if (n == 99)  { return GUI_KEY_F1 + 2; }
    if (n == 118) { return GUI_KEY_F1 + 3; }
    if (n == 96)  { return GUI_KEY_F1 + 4; }
    if (n == 97)  { return GUI_KEY_F1 + 5; }
    if (n == 98)  { return GUI_KEY_F1 + 6; }
    if (n == 100) { return GUI_KEY_F1 + 7; }
    if (n == 101) { return GUI_KEY_F1 + 8; }
    if (n == 109) { return GUI_KEY_F1 + 9; }
    if (n == 103) { return GUI_KEY_F1 + 10; }
    if (n == 111) { return GUI_KEY_F1 + 11; }
    // ANSI letter and digit keys (kVK_ANSI_*): position-based like X11's
    // unshifted keysym and Win32's virtual key.
    if (n >= 0 && n <= 50) {
        const char* map = "ASDFHGZXCV?BQWERYT123465=97-80]OU[IP?LJ'K;\\,/NM.";
        char c;
        unsafe { c = map[n]; }
        if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { return (int)c; }
    }
    return GUI_KEY_UNKNOWN;
}

static int __gui_cocoa_mods(unsigned long flags) {
    int m = 0;
    if ((flags & (1UL << 17)) != 0UL) { m = m | GUI_MOD_SHIFT; }
    if ((flags & (1UL << 18)) != 0UL) { m = m | GUI_MOD_CTRL; }
    if ((flags & (1UL << 19)) != 0UL) { m = m | GUI_MOD_ALT; }
    if ((flags & (1UL << 20)) != 0UL) { m = m | GUI_MOD_META; }
    return m;
}

// Routes one NSEvent; returns 1 if it should also go to [NSApp sendEvent:].
static int __gui_cocoa_route(ObjcId_* ev) {
    unsafe {
        MsgSendRetULong typeMsg = (MsgSendRetULong)objc_msgSend;
        unsigned long etype = typeMsg(ev, __gui_cocoa_sel("type"));
        int isKey = (etype == 10UL || etype == 11UL || etype == 12UL) ? 1 : 0;
        if (etype == 15UL) { return 0; }                 // our own wake() event
        MsgSend0 winMsg = (MsgSend0)objc_msgSend;
        ObjcId_* nsWin = winMsg(ev, __gui_cocoa_sel("window"));
        if (nsWin == null && isKey) { nsWin = winMsg(gNSApp, __gui_cocoa_sel("keyWindow")); }
        if (nsWin == null) { return isKey ? 0 : 1; }
        ?&GuiWindowState ost = __gui_cocoa_find(nsWin);
        if (ost.is_null()) { return isKey ? 0 : 1; }
        struct GuiWindowState* st = (struct GuiWindowState*)ost;

        MsgSendRetULong flagsMsg = (MsgSendRetULong)objc_msgSend;
        int mods = __gui_cocoa_mods(flagsMsg(ev, __gui_cocoa_sel("modifierFlags")));

        int isMouse = (etype >= 1UL && etype <= 7UL) || etype == 22UL || etype == 25UL || etype == 26UL || etype == 27UL;
        double ex = 0.0;
        double ey = 0.0;
        if (isMouse) {
            MsgSendPoint locMsg = (MsgSendPoint)objc_msgSend;
            struct NSPoint loc = locMsg(ev, __gui_cocoa_sel("locationInWindow"));
            // Window coordinates have y up from the content's bottom edge.
            ex = loc.x * st->scale;
            ey = ((double)st->logicalH - loc.y) * st->scale;
        }

        if (etype == 1UL || etype == 3UL || etype == 25UL || etype == 2UL || etype == 4UL || etype == 26UL) {
            int down = (etype == 1UL || etype == 3UL || etype == 25UL) ? 1 : 0;
            struct GuiEvent e = GuiEvent::new(down ? GUI_EVENT_MOUSE_DOWN : GUI_EVENT_MOUSE_UP);
            e.x = ex; e.y = ey;
            e.button = (etype == 1UL || etype == 2UL) ? GUI_BUTTON_LEFT
                     : ((etype == 3UL || etype == 4UL) ? GUI_BUTTON_RIGHT : GUI_BUTTON_MIDDLE);
            // Control-click is the one-button right click.
            if (e.button == GUI_BUTTON_LEFT && (mods & GUI_MOD_CTRL) != 0) { e.button = GUI_BUTTON_RIGHT; }
            e.modifiers = mods;
            gui_backend_post(st, e);
            return 1;
        }
        if (etype == 5UL || etype == 6UL || etype == 7UL || etype == 27UL) {
            int inside = (ex >= 0.0 && ey >= 0.0 && ex < (double)st->pixelW && ey < (double)st->pixelH) ? 1 : 0;
            if (inside && !st->hovered) {
                struct GuiEvent en = GuiEvent::new(GUI_EVENT_MOUSE_ENTER);
                en.x = ex; en.y = ey;
                gui_backend_post(st, en);
                __gui_cocoa_apply_cursor(st->cursor);
            } else if (!inside && st->hovered && etype == 5UL) {
                struct GuiEvent lv = GuiEvent::new(GUI_EVENT_MOUSE_LEAVE);
                lv.x = ex; lv.y = ey;
                gui_backend_post(st, lv);
                __gui_cocoa_apply_cursor(GUI_CURSOR_ARROW);
            }
            struct GuiEvent e = GuiEvent::new(GUI_EVENT_MOUSE_MOVE);
            e.x = ex; e.y = ey;
            e.modifiers = mods;
            gui_backend_post(st, e);
            return 1;
        }
        if (etype == 22UL) {
            MsgSendDouble dxMsg = (MsgSendDouble)objc_msgSend;
            double dx = dxMsg(ev, __gui_cocoa_sel("scrollingDeltaX"));
            double dy = dxMsg(ev, __gui_cocoa_sel("scrollingDeltaY"));
            MsgSendRetBool preciseMsg = (MsgSendRetBool)objc_msgSend;
            if (preciseMsg(ev, __gui_cocoa_sel("hasPreciseScrollingDeltas")) != (signed char)0) {
                dx = dx / 10.0;   // trackpad points to "lines"
                dy = dy / 10.0;
            }
            struct GuiEvent e = GuiEvent::new(GUI_EVENT_SCROLL);
            e.x = ex; e.y = ey;
            e.scrollDx = dx;
            e.scrollDy = dy;
            e.modifiers = mods;
            gui_backend_post(st, e);
            return 1;
        }
        if (etype == 10UL || etype == 11UL) {
            MsgSendRetUShort kcMsg = (MsgSendRetUShort)objc_msgSend;
            unsigned short nativeKc = kcMsg(ev, __gui_cocoa_sel("keyCode"));
            struct GuiEvent e = GuiEvent::new(etype == 10UL ? GUI_EVENT_KEY_DOWN : GUI_EVENT_KEY_UP);
            e.keycode = __gui_cocoa_keycode(nativeKc);
            e.modifiers = mods;
            if (etype == 10UL && (mods & (GUI_MOD_META | GUI_MOD_CTRL)) == 0) {
                // The typed text: [event characters] (an NSString) as UTF-8.
                MsgSend0 charsMsg = (MsgSend0)objc_msgSend;
                ObjcId_* nsChars = charsMsg(ev, __gui_cocoa_sel("characters"));
                if (nsChars != null) {
                    MsgSend0 utf8Msg = (MsgSend0)objc_msgSend;
                    const char* s = (const char*)utf8Msg(nsChars, __gui_cocoa_sel("UTF8String"));
                    if (s != (const char*)0) {
                        unsigned long n = 0UL;
                        while (s[n] != '\0') { n = n + 1UL; }
                        unsigned long pos = 0UL;
                        unsigned int c = gui_utf8_next_n_(s, n, &pos);
                        // Function keys come as private-use characters.
                        if (c >= 32U && c != 127U && !(c >= 0xF700U && c <= 0xF8FFU)) { e.codepoint = c; }
                        gui_backend_post(st, e);
                        while (pos < n) {
                            unsigned int c2 = gui_utf8_next_n_(s, n, &pos);
                            if (c2 >= 32U && c2 != 127U && !(c2 >= 0xF700U && c2 <= 0xF8FFU)) {
                                struct GuiEvent ce = GuiEvent::new(GUI_EVENT_CHAR);
                                ce.codepoint = c2;
                                gui_backend_post(st, ce);
                            }
                        }
                        return 0;
                    }
                }
            }
            gui_backend_post(st, e);
            return 0;
        }
        return isKey ? 0 : 1;
    }
}

static int __gui_cocoa_pump(int timeoutMs) {
    int any = 0;
    unsafe {
        ObjcPool_* pool = objc_autoreleasePoolPush();
        MsgSend0 dateMsg = (MsgSend0)objc_msgSend;
        ObjcId_* dateCls = objc_getClass("NSDate");
        MsgSendNextEvent nextEvent = (MsgSendNextEvent)objc_msgSend;
        MsgSendVoid1 sendEvent = (MsgSendVoid1)objc_msgSend;
        int first = 1;
        while (1) {
            ObjcId_* until;
            if (first && timeoutMs != 0 && !gui_backend_has_events()) {
                if (timeoutMs < 0) {
                    until = dateMsg(dateCls, __gui_cocoa_sel("distantFuture"));
                } else {
                    MsgSendDoubleArg sinceNow = (MsgSendDoubleArg)objc_msgSend;
                    until = sinceNow(dateCls, __gui_cocoa_sel("dateWithTimeIntervalSinceNow:"),
                                     (double)timeoutMs / 1000.0);
                }
            } else {
                until = dateMsg(dateCls, __gui_cocoa_sel("distantPast"));
            }
            first = 0;
            ObjcId_* ev = nextEvent(gNSApp, __gui_cocoa_sel("nextEventMatchingMask:untilDate:inMode:dequeue:"),
                                 0xFFFFFFFFFFFFFFFFUL, until, gDefaultMode, 1);
            if (ev == null) { break; }
            if (__gui_cocoa_route(ev)) { sendEvent(gNSApp, __gui_cocoa_sel("sendEvent:"), ev); }
            any = 1;
        }
        MsgSendVoid0 updateWindows = (MsgSendVoid0)objc_msgSend;
        updateWindows(gNSApp, __gui_cocoa_sel("updateWindows"));
        objc_autoreleasePoolPop(pool);
    }
    return any;
}

static void __gui_cocoa_wake() {
    unsafe {
        ObjcPool_* pool = objc_autoreleasePoolPush();
        struct NSPoint p;
        p.x = 0.0;
        p.y = 0.0;
        MsgSendOtherEvent other = (MsgSendOtherEvent)objc_msgSend;
        ObjcId_* ev = other(objc_getClass("NSEvent"),
                         __gui_cocoa_sel("otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:"),
                         15UL /* NSEventTypeApplicationDefined */, p, 0UL, 0.0, 0L, null, (short)0, 0L, 0L);
        if (ev != null) {
            MsgSendVoid1Int post = (MsgSendVoid1Int)objc_msgSend;
            post(gNSApp, __gui_cocoa_sel("postEvent:atStart:"), ev, 0);
        }
        objc_autoreleasePoolPop(pool);
    }
}

static long long __gui_cocoa_now_ns() { return time_mono_ns(); }

static int __gui_cocoa_clip_set(const &char utf8, unsigned long n) {
    unsafe {
        ObjcPool_* pool = objc_autoreleasePoolPush();
        MsgSend0 m = (MsgSend0)objc_msgSend;
        ObjcId_* pb = m(objc_getClass("NSPasteboard"), __gui_cocoa_sel("generalPasteboard"));
        if (pb == null) { objc_autoreleasePoolPop(pool); return 0; }
        MsgSendRetLong clearMsg = (MsgSendRetLong)objc_msgSend;
        clearMsg(pb, __gui_cocoa_sel("clearContents"));
        // NUL-terminated copy for stringWithUTF8String:.
        char* z = (char*)alloc_raw_(n + 1UL);
        safe_memcpy(z[0..n], ((const char*)utf8)[0..n]);
        z[n] = '\0';
        ObjcId_* str = __gui_cocoa_str(z);
        dealloc(z);
        MsgSendBool2 setStr = (MsgSendBool2)objc_msgSend;
        signed char ok = setStr(pb, __gui_cocoa_sel("setString:forType:"), str, gPasteType);
        objc_autoreleasePoolPop(pool);
        return ok != (signed char)0 ? 1 : 0;
    }
}

static int __gui_cocoa_clip_get(&String out) {
    out.clear();
    unsafe {
        ObjcPool_* pool = objc_autoreleasePoolPush();
        MsgSend0 m = (MsgSend0)objc_msgSend;
        ObjcId_* pb = m(objc_getClass("NSPasteboard"), __gui_cocoa_sel("generalPasteboard"));
        int ok = 0;
        if (pb != null) {
            MsgSend1 get = (MsgSend1)objc_msgSend;
            ObjcId_* str = get(pb, __gui_cocoa_sel("stringForType:"), gPasteType);
            if (str != null) {
                MsgSendRetStr utf8Msg = (MsgSendRetStr)objc_msgSend;
                const char* s = utf8Msg(str, __gui_cocoa_sel("UTF8String"));
                if (s != (const char*)0) {
                    out.push(s);
                    ok = 1;
                }
            }
        }
        objc_autoreleasePoolPop(pool);
        return ok;
    }
}

static double __gui_cocoa_display_scale() {
    unsafe {
        MsgSend0 m = (MsgSend0)objc_msgSend;
        ObjcId_* screen = m(objc_getClass("NSScreen"), __gui_cocoa_sel("mainScreen"));
        if (screen == null) { return 1.0; }
        MsgSendDouble scaleMsg = (MsgSendDouble)objc_msgSend;
        double s = scaleMsg(screen, __gui_cocoa_sel("backingScaleFactor"));
        return s > 0.0 ? s : 1.0;
    }
}

static struct GuiNativeHandle __gui_cocoa_native(&GuiWindowState st) {
    struct GuiNativeHandle h;
    h.kind = GUI_NATIVE_COCOA;
    h.display = 0ULL;
    h.window = st.key;
    h.view = 0ULL;
    h.visual = 0ULL;
    unsafe {
        h.display = (unsigned long long)(unsigned long)gNSApp;
        struct GuiCocoaWindow* w = (struct GuiCocoaWindow*)st.native;
        if (w != (struct GuiCocoaWindow*)0) { h.view = (unsigned long long)(unsigned long)w->view; }
    }
    return h;
}

static &GuiBackend __gui_native_backend() {
    if (!gCocoaBackendInit) {
        gCocoaBackend.name = "cocoa";
        gCocoaBackend.primaryMod = GUI_MOD_META;
        gCocoaBackend.init = __gui_cocoa_init;
        gCocoaBackend.window_create = __gui_cocoa_create;
        gCocoaBackend.window_destroy = __gui_cocoa_destroy;
        gCocoaBackend.window_present = __gui_cocoa_present;
        gCocoaBackend.window_set_title = __gui_cocoa_set_title;
        gCocoaBackend.window_set_size = __gui_cocoa_set_size;
        gCocoaBackend.window_set_position = __gui_cocoa_set_position;
        gCocoaBackend.window_update_limits = __gui_cocoa_apply_limits;
        gCocoaBackend.window_set_visible = __gui_cocoa_set_visible;
        gCocoaBackend.window_focus = __gui_cocoa_focus;
        gCocoaBackend.window_set_cursor = __gui_cocoa_set_cursor;
        gCocoaBackend.pump = __gui_cocoa_pump;
        gCocoaBackend.wake = __gui_cocoa_wake;
        gCocoaBackend.now_ns = __gui_cocoa_now_ns;
        gCocoaBackend.clipboard_set = __gui_cocoa_clip_set;
        gCocoaBackend.clipboard_get = __gui_cocoa_clip_get;
        gCocoaBackend.display_scale = __gui_cocoa_display_scale;
        gCocoaBackend.native_handle = __gui_cocoa_native;
        gCocoaBackendInit = 1;
    }
    return &gCocoaBackend;
}

// ── system-font text (Cocoa-only) ───────────────────────────────────────────
// Real anti-aliased system-font text, via Core Graphics'
// CGContextShowTextAtPoint (a plain-C, no-Objective-C-message-send text
// API, unlike the rest of AppKit — deprecated since 10.9 in favor of Core
// Text, but still present and functional). Kept for compatibility: the
// portable way to draw TrueType text on every backend is gui_canvas.h's
// GuiCanvas::fill_text() with a gui_text.h GuiTypeface (e.g. loaded from
// /System/Library/Fonts). Call this only from code that already branches
// on '#ifdef __APPLE__'. It stays a free function rather than a GuiWindow
// method: only this backend defines it.
extern CGContext_* CGBitmapContextCreate(unsigned char* data, unsigned long width, unsigned long height,
                                         unsigned long bitsPerComponent, unsigned long bytesPerRow,
                                         CGColorSpace_* colorSpace, unsigned int bitmapInfo);
extern void  CGContextRelease(CGContext_* ctx);
extern void  CGContextSetRGBFillColor(CGContext_* ctx, double r, double g, double b, double a);
extern void  CGContextSelectFont(CGContext_* ctx, const char* fontName, double size, unsigned int textEncoding);
extern void  CGContextSetTextDrawingMode(CGContext_* ctx, unsigned int mode);
extern void  CGContextShowTextAtPoint(CGContext_* ctx, double x, double y, const char* text, unsigned long length);

int gui_draw_text_system(&GuiWindow win, int x, int y, const &char text,
                          struct GuiColor color, double fontSize, const &char fontName) {
    unsafe {
        CGColorSpace_* space = CGColorSpaceCreateDeviceRGB();
        unsigned long rowBytes = (unsigned long)win.width * 4UL;
        // Draws directly into win.pixels (no separate blit) — same
        // buffer GuiWindow::present() reads every frame.
        CGContext_* ctx = CGBitmapContextCreate((unsigned char*)win.pixels, (unsigned long)win.width,
                                           (unsigned long)win.height, 8UL, rowBytes, space, 1U);
        if (ctx == null) { CGColorSpaceRelease(space); return 0; }

        CGContextSetRGBFillColor(ctx, (double)color.r / 255.0, (double)color.g / 255.0,
                                  (double)color.b / 255.0, (double)color.a / 255.0);
        CGContextSelectFont(ctx, fontName, fontSize, 1U /* kCGEncodingMacRoman */);
        CGContextSetTextDrawingMode(ctx, 0U /* kCGTextFill */);

        unsigned long textLen = 0UL;
        while (text[textLen] != '\0') { textLen = textLen + 1UL; }
        // CGBitmapContextCreate's coordinate origin is bottom-left; flip
        // Y (and nudge up by the font size so 'y' means "top of the text"
        // like GuiWindow::draw_text(), not the Quartz baseline).
        double cgY = (double)win.height - (double)y - fontSize;
        CGContextShowTextAtPoint(ctx, (double)x, cgY, text, textLen);

        CGContextRelease(ctx);
        CGColorSpaceRelease(space);
        return 1;
    }
}

// $SAFEC_GUI_BACKEND=headless selects the in-memory backend (gui_headless.h).
&GuiBackend gui_platform_backend() {
    if (gui_headless_requested()) { return gui_headless_backend(); }
    return __gui_native_backend();
}

} // namespace std