module ui_cocoa

module lib/ui_cocoa.mu

import "ui_cocoa"

ui_cocoa is the macOS backend behind ui: widgets are real AppKit controls reached through objc, and the event loop pumps NSApplication while draining the action queue Cocoa fills. Programs import ui, never this module; ui hands every call here on darwin.

The shared state map u is owned by ui. This backend keeps its own keys in it: "app" (NSApplication), "queue" (the NSMutableArray every control's target/action appends the sender to), "menued", plus the shared "windows", "widgets" (sender pointer → widget handle) and "handlers" (sender pointer → closure) it dispatches with.

Imports

Functions

backend#

fn backend()

backend answers the function table ui dispatches through.

Source lib/ui_cocoa.mu:16
fn backend() {
    return {"init": init, "window": window, "label": label, "button": button, "input": input, "checkbox": checkbox, "textarea": textarea, "set_text": set_text, "get_text": get_text, "get_checked": get_checked, "set_checked": set_checked, "place": place, "remove": remove, "run": run}
}

button#

fn button(u, win, title, x, y, w, h, onclick)

button places a push button whose click lands in onclick(widget).

Source lib/ui_cocoa.mu:97
fn button(u, win, title, x, y, w, h, onclick) {
    // Cocoa itself queues the sender: the target is our NSMutableArray and
    // the action is addObject:, so a click appends the button to the queue
    // and run() maps it back to onclick.
    btn := objc.send(objc.cls("NSButton"), "buttonWithTitle:target:action:", objc.nsstring(title), u["queue"], objc.sel("addObject:"))

    if is_error(btn) {
        return btn
    }

    handle := _place_view(u, win, btn, "button", x, y, w, h)

    if is_error(handle) {
        return handle
    }

    handle["onclick"] = onclick
    return handle
}

checkbox#

fn checkbox(u, win, title, x, y, w, h, onchange)

checkbox places a toggle whose change lands in onchange(widget).

Source lib/ui_cocoa.mu:142
fn checkbox(u, win, title, x, y, w, h, onchange) {
    cb := objc.send(objc.cls("NSButton"), "checkboxWithTitle:target:action:", objc.nsstring(title), u["queue"], objc.sel("addObject:"))

    if is_error(cb) {
        return cb
    }

    handle := _place_view(u, win, cb, "checkbox", x, y, w, h)

    if is_error(handle) {
        return handle
    }

    handle["onchange"] = onchange
    return handle
}

get_checked#

fn get_checked(_, widget)

get_checked answers a checkbox's state.

Source lib/ui_cocoa.mu:213
fn get_checked(_, widget) {
    return objc.send(widget["ptr"], "state") & 255 == 1
}

get_text#

fn get_text(_, widget)

get_text answers a widget's current text.

Source lib/ui_cocoa.mu:197
fn get_text(_, widget) {
    kind := widget["kind"]

    if kind == "button" || kind == "checkbox" || kind == "window" {
        return objc.utf8(objc.send(widget["ptr"], "title"))
    }

    if kind == "textarea" {
        return objc.utf8(objc.send(widget["ptr"], "string"))
    }

    return objc.utf8(objc.send(widget["ptr"], "stringValue"))
}

init#

fn init(u)

init brings up the application object and the action queue.

Source lib/ui_cocoa.mu:22
fn init(u) {
    if u["app"] != nil {
        return true
    }

    ok := objc.init()

    if is_error(ok) {
        return ok
    }

    app := objc.send(objc.cls("NSApplication"), "sharedApplication")

    if is_error(app) {
        return app
    }

    // NSApplicationActivationPolicyRegular: a real app with a Dock icon that
    // can come to the foreground.
    objc.send(app, "setActivationPolicy:", 0)
    queue := objc.send(objc.send(objc.cls("NSMutableArray"), "alloc"), "init")

    if is_error(queue) {
        return queue
    }

    u["app"] = app
    u["queue"] = queue
    return true
}

input#

fn input(u, win, text, x, y, w, h, onsubmit)

input places an editable text field; Return lands in onsubmit(widget).

Source lib/ui_cocoa.mu:119
fn input(u, win, text, x, y, w, h, onsubmit) {
    fld := objc.send(objc.cls("NSTextField"), "textFieldWithString:", objc.nsstring(text))

    if is_error(fld) {
        return fld
    }

    handle := _place_view(u, win, fld, "input", x, y, w, h)

    if is_error(handle) {
        return handle
    }

    // Wire the action unconditionally so a handler assigned to the handle
    // after creation still fires, the way the X11 backend reads it live.
    objc.send(fld, "setTarget:", u["queue"])
    objc.send(fld, "setAction:", objc.sel("addObject:"))
    handle["onsubmit"] = onsubmit
    return handle
}

label#

fn label(u, win, text, x, y, w, h)

label places a wrapping static text label.

Source lib/ui_cocoa.mu:85
fn label(u, win, text, x, y, w, h) {
    lbl := objc.send(objc.cls("NSTextField"), "wrappingLabelWithString:", objc.nsstring(text))

    if is_error(lbl) {
        return lbl
    }

    return _place_view(u, win, lbl, "label", x, y, w, h)
}

place#

fn place(_, widget, x, y, w, h)

place moves and resizes a view (top-left coordinates, flipped through the window's CURRENT content height, so it stays right after a resize).

Source lib/ui_cocoa.mu:232
fn place(_, widget, x, y, w, h) {
    win := widget["win"]
    flipped := _content_h(win) - y - h
    r := objc.invoke(widget["ptr"], "setFrame:", [objc.rect(x, flipped, w, h)])

    if is_error(r) {
        return r
    }

    return nil
}

remove#

fn remove(u, widget)

remove takes a view out of its window and forgets its handler.

Source lib/ui_cocoa.mu:246
fn remove(u, widget) {
    objc.send(widget["ptr"], "removeFromSuperview")
    key := str(widget["ptr"])

    if has(u["handlers"], key) {
        del(u["handlers"], key)
    }

    if has(u["widgets"], key) {
        del(u["widgets"], key)
    }

    return nil
}

run#

fn run(u)

run pumps the Cocoa event loop until u["running"] goes false or the last window closes.

Source lib/ui_cocoa.mu:264
fn run(u) {
    app := u["app"]
    _ensure_menu(u)
    objc.send(app, "activateIgnoringOtherApps:", true)
    objc.send(app, "finishLaunching")
    mode := objc.nsstring("kCFRunLoopDefaultMode")

    while u["running"] {
        pool := objc.pool()
        _show_new_windows(u)
        // Fire due timers; the budget until the next one bounds the wait.
        tick := u["tick"]
        budget := tick()
        // Block until an event or the next timer: idle costs nothing.
        date := _wait_date(budget)
        e := objc.send(app, "nextEventMatchingMask:untilDate:inMode:dequeue:", 0 - 1, date, mode, 1)

        if !is_error(e) && e != 0 {
            objc.send(app, "sendEvent:", e)
            // Drain whatever queued behind it without blocking again.
            n := 0

            while n < 100 {
                e = objc.send(app, "nextEventMatchingMask:untilDate:inMode:dequeue:", 0 - 1, 0, mode, 1)

                if is_error(e) || e == 0 {
                    break
                }

                objc.send(app, "sendEvent:", e)
                n = n + 1
            }
        }

        _dispatch(u)
        _poll_closed(u)

        if !_any_window_open(u) {
            u["running"] = false
        }

        objc.drain(pool)
    }

    return nil
}

set_checked#

fn set_checked(_, widget, v)

set_checked flips a checkbox programmatically.

Source lib/ui_cocoa.mu:219
fn set_checked(_, widget, v) {
    state := 0

    if v {
        state = 1
    }

    return objc.send(widget["ptr"], "setState:", state)
}

set_text#

fn set_text(_, widget, text)

set_text replaces a widget's text.

Source lib/ui_cocoa.mu:180
fn set_text(_, widget, text) {
    ns := objc.nsstring(text)
    kind := widget["kind"]

    if kind == "button" || kind == "checkbox" || kind == "window" {
        return objc.send(widget["ptr"], "setTitle:", ns)
    }

    if kind == "textarea" {
        return objc.send(widget["ptr"], "setString:", ns)
    }

    return objc.send(widget["ptr"], "setStringValue:", ns)
}

textarea#

fn textarea(u, win, text, x, y, w, h)

textarea places a multi-line editable text view (fixed size, no scrolling).

Source lib/ui_cocoa.mu:161
fn textarea(u, win, text, x, y, w, h) {
    tv := objc.send(objc.send(objc.cls("NSTextView"), "alloc"), "init")

    if is_error(tv) {
        return tv
    }

    handle := _place_view(u, win, tv, "textarea", x, y, w, h)

    if is_error(handle) {
        return handle
    }

    objc.send(tv, "setString:", objc.nsstring(text))
    return handle
}

window#

fn window(_, title, w, h)

window creates a titled, closable, resizable window of the given content size, hidden until run() shows it.

Source lib/ui_cocoa.mu:56
fn window(_, title, w, h) {
    win := objc.send(objc.cls("NSWindow"), "alloc")

    if is_error(win) {
        return win
    }

    // styleMask 15 = titled | closable | miniaturizable | resizable;
    // backing 2 = buffered. The rect goes through NSInvocation: it is a
    // float aggregate the plain FFI cannot pass.
    inv := objc.invoke(win, "initWithContentRect:styleMask:backing:defer:", [objc.rect(0, 0, w, h), _pack_int(15), _pack_int(2), _pack_int(0)])

    if is_error(inv) {
        return inv
    }

    out := buf(8)
    objc.send(inv, "getReturnValue:", out)
    win = _unpack_int(out)
    objc.send(win, "setTitle:", objc.nsstring(title))
    objc.send(win, "center")
    // Closing must hide the window, never deallocate it: the µ handle keeps
    // pointing at it, and run() polls it to notice the close.
    objc.send(win, "setReleasedWhenClosed:", false)
    return {"kind": "window", "ptr": win, "w": w, "h": h, "_shown": false, "_closed": false, "onclose": nil}
}

Internal helpers

Underscore-prefixed names are implementation detail. They are listed so the module's source reads without surprises, not as API — they may change at any time.

_any_window_open(u)
_content_h(win)
_dispatch(u)
_ensure_menu(u)
_pack_int(v)
_place_view(u, win, view, kind, x, y, w, h)
_poll_closed(u)
_show_new_windows(u)
_unpack_int(b)
_wait_date(budget)
_window_open(p)