cocoa: back toolkit.Native with embedded AppKit controls (syncNative) - #86
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
`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.