Native views on Tao
Embed any platform-native view — NSView, HWND, or GtkWidget — inside a Compose layout on the Tao backend.
The NativeView composable embeds a platform-native view inside a Compose layout on the
Tao backend. It is the Tao equivalent of AndroidView on Android and UIKitView on
Compose iOS: you supply a NucleusPlatformView (NSView*, HWND, or GtkWidget*), and
Nucleus reparents that handle into the Tao window, keeps it sized to the Compose layout
slot, and disposes it when the composable leaves. Anything that exposes one of those
handles works — AppKit/SwiftUI views, Win32 or WinUI controls, WebView2, GTK widgets, and
so on.
Add the dependency
plugins {
id("dev.nucleusframework")
}
dependencies {
implementation("dev.nucleusframework:nucleus.nucleus-application:2.5.4")
implementation("dev.nucleusframework:nucleus.decorated-window-tao:2.5.4")
}NativeView and NucleusPlatformView live in dev.nucleusframework.window.tao, provided
by decorated-window-tao. They are available inside any window opened on the Tao backend.
Embed a native view
Wrap your platform handle in a NucleusPlatformView and pass a factory to NativeView.
The variant you implement decides the embedding strategy — NsView on macOS, HWnd on
Windows, GtkWidget on Linux:
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import dev.nucleusframework.window.tao.NativeView
import dev.nucleusframework.window.tao.NucleusPlatformView
// A macOS view backed by a raw NSView* handle you obtain from your
// own AppKit / SwiftUI bridge.
class WebPlatformView(private val handle: Long) : NucleusPlatformView.NsView {
override val nsViewHandle: Long = handle
override fun dispose() {
// Release the native view you created in the factory.
}
}
@Composable
fun BrowserPane(url: String) {
NativeView(
factory = { WebPlatformView(createNativeWebView(url)) },
modifier = Modifier.fillMaxSize(),
)
}factory runs once and its result is remembered for the lifetime of the composable.
Nucleus calls dispose() when NativeView leaves the composition. The native view
tracks the layout slot's position and size automatically — you don't set frames yourself.
Nucleus does not create the native view for you. createNativeWebView above is a
placeholder for your own interop code that returns the platform handle (an NSView* on
macOS, an HWND on Windows, a GtkWidget* on Linux) as a Long.
How it works
NativeView reads the variant of the NucleusPlatformView returned by factory and
routes to the matching embedding path. On macOS the NsView is added as a sibling of the
Tao content view; on Windows the HWnd is reparented under the main Tao HWND; on Linux
the GtkWidget is added to Tao's GTK content widget. Each path tracks the Compose layout
slot with onGloballyPositioned and forwards the bounds to the native side, so the
embedded view follows resizes and scrolls with no extra wiring.
If the returned variant doesn't match the current OS — or the platform's scene plumbing
isn't available — NativeView renders an empty Box(modifier) instead of failing. This
lets you write one call site per platform and rely on the fallback on the others.
The optional content slot renders Compose UI on top of the embedded view, in the host window's
own Compose scene on all three platforms — the same hole-punch blending path as later Compose
siblings and in-scene popups. Overlay content claims its own input; anything it does not cover
falls through to the native view, with no marker modifier needed.
API reference
Describe the native view
NucleusPlatformView is a sealed interface. Implement exactly one platform variant and
expose the corresponding handle as a Long:
| Variant | Platform | Handle property |
|---|---|---|
NucleusPlatformView.NsView | macOS | nsViewHandle — pointer to your NSView* |
NucleusPlatformView.HWnd | Windows | hwndHandle — pointer to your HWND |
NucleusPlatformView.GtkWidget | Linux | gtkWidgetHandle — pointer to your GtkWidget* |
The interface also declares lifecycle callbacks with default no-op bodies. Override the ones your view needs:
| Member | Called when |
|---|---|
resize(widthPx, heightPx) | The embedded view's logical size changes. |
setBounds(xPx, yPx, widthPx, heightPx) | The full frame changes; override when the view's drawing rect is decoupled from its window rect (for example, WebView2). |
setCornerRadius(radiusPx) | The rounded-corner clip changes and the host's generic clip can't reach the view's surface. |
clearFocus() | The host window or a sibling Compose layer takes focus. |
dispose() | Final teardown — release the native resources you own. |
Embed the view
@Composable
fun NativeView(
factory: () -> NucleusPlatformView,
modifier: Modifier = Modifier,
update: (NucleusPlatformView) -> Unit = {},
cornerRadius: Dp = Dp.Unspecified,
content: @Composable () -> Unit = {},
)| Parameter | Notes |
|---|---|
factory | Creates the NucleusPlatformView. Runs once; the result is remembered and disposed when the composable leaves. |
modifier | Sizes and positions the layout slot. The embedded view fills it. |
update | Runs on every recomposition with the remembered view. Use it to push new state into the native view. |
cornerRadius | Rounded-corner clip; see below. |
content | Compose overlay rendered on top of the native view. |
Overlay Compose content
The content slot draws Compose UI over the embedded view, in the window's own Compose scene.
Anything you put there is an ordinary composable: it takes pointer events where it is drawn, and
events it does not consume are replayed to the native view underneath.
NativeView(
factory = { WebPlatformView(createNativeWebView(url)) },
modifier = Modifier.fillMaxSize(),
) {
// A Compose toolbar over the web view. The button handles its own clicks;
// clicks elsewhere reach the web view underneath.
Button(onClick = { /* reload */ }) {
Text("Reload")
}
}Because the overlay lives in the main scene, Compose siblings declared after the NativeView
also paint over it, and Popups from inside the scene land above the native view.
Modifier.consumeOverlayPointerEvents() is deprecated and no longer needed — overlay content
claims its own input. The cursor argument still maps to Modifier.pointerHoverIcon; use that
modifier directly instead.
The native view is composited into the scene with a punched hole: Compose clears the view's rectangle to transparent, and the platform shows the native content through it.
The NSView is inserted below the Tao content view, and the Metal layer becomes non-opaque so
Skia clears at alpha 0. Moves are applied in the same frame as the Compose geometry.
The child HWND is parented under the window. Win32 children always paint above their parent, so
Compose is re-rendered on top through a DirectComposition overlay clipped to the union of the
live NativeView rectangles — Compose still sees events first inside those rectangles.
A hwndHandle of 0L is legal: a view that has no Win32 child window (WebView2 in composition
mode, for example) drives its own visual through setBounds and setCornerRadius.
The GtkWidget is reparented under the window and the GL surface paints above it, so the punched
hole reveals the widget. While a NativeView is attached, the surface advertises an empty opaque
region so the compositor keeps blending the GTK content underneath.
This requires Wayland. On the X11 backend a child window's alpha is never blended against its parent, so the punched rectangle shows the desktop instead of the widget — the widget still runs, but behind the GL surface.
Clip to rounded corners
Compose's Modifier.clip() does not propagate to embedded native views, the same
limitation as AndroidView and UIKitView. Use cornerRadius instead:
NativeView(
factory = { WebPlatformView(createNativeWebView(url)) },
modifier = Modifier.fillMaxSize(),
cornerRadius = 12.dp,
)Pass Dp.Infinity to clip fully circular regardless of size. The default,
Dp.Unspecified, applies no clip.
cornerRadius has no effect on Linux, where GTK 3 has no per-widget rounded clipping. Draw a
Compose RoundedCornerShape border in the content slot instead.
What's next
- WebView — ready-made multiplatform WebView built on this primitive for desktop.
- TextureView — external GPU textures in the Compose scene (no CPU copy).
- The Tao backend — what the no-AWT backend covers.
- DecoratedWindow on Tao — open the window that hosts the view.
- Multi-touch and trackpad gestures — read Tao input events.