Nucleus
Tao backend

Native views on Tao

Embed a platform-native view — AppKit/SwiftUI, WebView2, or GTK — 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, and Nucleus reparents its underlying handle into the Tao window, keeps it sized to the Compose layout slot, and disposes it when the composable leaves.

Add the dependency

build.gradle.kts
plugins {
    id("dev.nucleusframework")
}

dependencies {
    implementation("dev.nucleusframework:nucleus.nucleus-application:2.0.5")
    implementation("dev.nucleusframework:nucleus.decorated-window-tao:2.0.5")
}

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 or WebView2 controller 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. On macOS it is a sibling overlay NSView with its own CAMetalLayer and ComposeScene; on Windows it is a top-level layered HWND composited via DirectComposition; on Linux it renders inline in the same window scene. Pointer events fall through to the native view except inside regions you mark with Modifier.consumeOverlayPointerEvents().

API reference

Describe the native view

NucleusPlatformView is a sealed interface. Implement exactly one platform variant and expose the corresponding handle as a Long:

VariantPlatformHandle property
NucleusPlatformView.NsViewmacOSnsViewHandle — pointer to your NSView*
NucleusPlatformView.HWndWindowshwndHandle — pointer to your HWND
NucleusPlatformView.GtkWidgetLinuxgtkWidgetHandle — pointer to your GtkWidget*

The interface also declares lifecycle callbacks with default no-op bodies. Override the ones your view needs:

MemberCalled 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 = {},
)
ParameterNotes
factoryCreates the NucleusPlatformView. Runs once; the result is remembered and disposed when the composable leaves.
modifierSizes and positions the layout slot. The embedded view fills it.
updateRuns on every recomposition with the remembered view. Use it to push new state into the native view.
cornerRadiusRounded-corner clip; see below.
contentCompose overlay rendered on top of the native view.

Overlay Compose content

The content slot draws Compose UI over the embedded view. By default native pointer events pass through it to the underlying view. Wrap an interactive region with Modifier.consumeOverlayPointerEvents() to route events in that region to Compose instead:

import dev.nucleusframework.window.tao.consumeOverlayPointerEvents

NativeView(
    factory = { WebPlatformView(createNativeWebView(url)) },
    modifier = Modifier.fillMaxSize(),
) {
    // A Compose toolbar over the web view; the button stays interactive,
    // clicks elsewhere reach the web view underneath.
    Button(
        onClick = { /* reload */ },
        modifier = Modifier.consumeOverlayPointerEvents(),
    ) {
        Text("Reload")
    }
}

For text-input fields, pass a cursor so the whole region shows the I-beam: consumeOverlayPointerEvents(cursor = PointerIcon.Text).

The content overlay is supported on macOS and Windows only. On Linux the GtkWidget variant ignores the content lambda.

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.

What's next