Skip to content

cocoa: back toolkit.Native with embedded AppKit controls (syncNative) - #86

Merged
tannevaled merged 1 commit into
mainfrom
feat/native-controls
Aug 31, 2026
Merged

cocoa: back toolkit.Native with embedded AppKit controls (syncNative)#86
tannevaled merged 1 commit into
mainfrom
feat/native-controls

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

The platform half of the native-control seam, and the consumer that earns `toolkit.Native` its place (it is not a bare rect like the retired `Foreign` — a real control's value flows back and it survives across frames).

A `toolkit.Native` marks where a control goes and holds its value; this backend embeds a real AppKit control (`go-macos/appkit` v0.1.0) as a subview over the framebuffer, binds its value both ways, and keeps it across frames.

How it differs from the Material seam

  • On top, not behind. A control is interactive and opaque, so it sits above the pixel view — no hole to punch; the toolkit suppresses the claimed Native's fallback draw.
  • Reconciled, not rebuilt. A control holds focus, a selection and an insertion point, so `syncNative` finds the same control by key (the Native's `Key`, else its stable address) and only moves it — rebuilding it each frame like a Material would lose what the person is typing.
  • A re-entrancy guard breaks the echo between the two binding directions, so a keystroke written into the model doesn't reset the field being typed in.

`syncNative` runs after each frame's layout — deferred in `paintFrame`, and at `bindAndSeed`/`resize` — so a control tracks its Native through scroll and interaction. A tree with no Native leaves every path untouched.

Proof

A gated live test (`WINDOW_COCOA_INTEGRATION`, run on-device) opens a real window with a Native secure field + button and verifies: two live controls embedded, the `NSSecureTextField` value round-trips, the `toolkit→native` binding drives the field from the model, and a control is reconciled away when its Native leaves the tree. The `nativeKey` unit test runs in normal CI.

Deps

Requires `go-widgets/toolkit` v0.298.0 (Native) and `go-macos/appkit` v0.1.0.

Linux

The seam is platform-neutral (`toolkit.Native` lives in the pure-Go toolkit); a GTK backend for Linux is a sibling of this file, tracked separately.

The platform half of the native-control seam. A toolkit.Native marks where a real
control goes and holds its value; here the cocoa backend embeds an actual AppKit
control (github.com/go-macos/appkit) as a subview over the framebuffer view,
binds its value to the Native's observables both ways, and keeps it across frames
so focus and text survive relayout.

It differs from the Material seam in two ways that matter. A control sits ON TOP
of the pixel view — interactive and opaque — so there is no hole to punch, and
the toolkit suppresses the claimed Native's fallback draw so nothing shows
through. And controls are RECONCILED, not rebuilt: a Material is a passive
rectangle rebuilt each layout, but a control holds focus, a selection and an
insertion point, so syncNative finds the same control again by key (the Native's
Key, else its stable address) and only moves it. A re-entrancy guard breaks the
echo between the two binding directions so a keystroke written into the model
does not reset the field the person is typing in.

syncNative runs after each frame's layout — deferred in paintFrame, and at
bindAndSeed and resize — so a control tracks its Native through scrolling and
interaction. A tree with no Native leaves every path untouched.

Proven end to end by a gated live test (WINDOW_COCOA_INTEGRATION): a real window
with a Native secure field and button embeds two live controls, round-trips the
secure field's value, drives the toolkit->native binding, and reconciles a
control away when its Native leaves the tree.

Requires go-widgets/toolkit v0.298.0 (Native) and go-macos/appkit v0.1.0.
@tannevaled
tannevaled merged commit a1121ee into main Aug 31, 2026
27 checks passed
@tannevaled
tannevaled deleted the feat/native-controls branch August 31, 2026 10:01
tannevaled added a commit that referenced this pull request Aug 31, 2026
…t tree) (#88)

Reworks syncNative onto go-widgets/toolkit's NativeControl descriptor
(toolkit v0.299.0). The backend gathers this frame's controls from the root's
own provider when it has one — a self-rendering toolkit.Surface, which is how the
news reader is built — else by walking it as a widget tree, and reconciles the
same descriptors either way, holding one real AppKit control per Key across
frames.

The value binding is immediate-mode-safe by construction: a descriptor's value is
pushed into a control only when it differs from what the control last reported.
When the person edits, the change flows out through the descriptor's callback and
the app's next descriptor carries that same value — equal, so nothing is pushed
back and the caret is never disturbed. Only a value the app changed on its own is
pushed. This replaces the observable-subscription binding of #86, which could not
serve a Surface app and pushed on every observable set rather than per frame.

Proven on-device by two gated live tests (WINDOW_COCOA_INTEGRATION): the
widget-tree path and the Surface-provider path — the reader's actual mechanism —
each embedding a real NSSecureTextField, round-tripping its value, and pushing an
app-side change into the field.

Requires go-widgets/toolkit v0.299.0 and go-macos/appkit v0.1.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant