Nucleus
Tao backend

DecoratedWindow on Tao

Open a Compose Desktop window on the Tao backend with a custom title bar slot and native window controls, without AWT.

DecoratedWindow is the same Composable on every Nucleus backend. On Tao it opens a native OS window, mounts a Skiko rendering surface, and gives you a TitleBar slot you fill with Compose content — including per-OS control-button layouts. This page covers the Tao-specific behavior of DecoratedWindow and the members exposed inside its content lambda.

Add the dependency

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

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

nucleus-application provides the unified entry point; decorated-window-tao provides the Tao backend. With both on the classpath, NucleusBackend.Tao is available.

Open a window

Start the runtime with nucleusApplication, then call DecoratedWindow and add a TitleBar:

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberWindowState
import dev.nucleusframework.application.DecoratedWindow
import dev.nucleusframework.application.NucleusBackend
import dev.nucleusframework.application.nucleusApplication
import dev.nucleusframework.window.NucleusDecoratedWindowTheme
import dev.nucleusframework.window.TitleBar
import dev.nucleusframework.window.macOSLargeCornerRadius
import dev.nucleusframework.window.styling.TitleBarColors
import dev.nucleusframework.window.styling.TitleBarMetrics
import dev.nucleusframework.window.styling.TitleBarStyle

fun main() = nucleusApplication(backend = NucleusBackend.Tao) {
    val titleBarStyle = TitleBarStyle(
        colors = TitleBarColors(
            background = Color(0xFF1A1D24),
            inactiveBackground = Color(0xFF15181D),
            content = Color(0xFFE6E6E6),
            border = Color.Transparent,
        ),
        metrics = TitleBarMetrics(height = 36.dp),
    )

    NucleusDecoratedWindowTheme(isDark = true, titleBarStyle = titleBarStyle) {
        DecoratedWindow(
            onCloseRequest = ::exitApplication,
            state = rememberWindowState(size = DpSize(1024.dp, 720.dp)),
            title = "Tao Demo",
            minimumSize = DpSize(640.dp, 480.dp),
        ) {
            TitleBar(modifier = Modifier.macOSLargeCornerRadius()) { state ->
                Text("Tao Demo", Modifier.align(Alignment.CenterHorizontally))
            }
            Box(Modifier.fillMaxSize()) { /* app content */ }
        }
    }
}

The TitleBar slot handles title-bar press-and-drag internally, so the window moves when you drag empty title-bar space. You don't wire up a drag modifier yourself.

For full-window layouts, custom design-system chrome, macOS glass sidebars, or Windows 11 Mica/Acrylic, use WindowScaffold instead of (or around) the stock TitleBar. Existing TitleBar call sites stay valid.

Open a window from composition

DecoratedWindow / DecoratedDialog are extensions on NucleusApplicationScope, a receiver that only exists at the top of main(). Secondary windows usually open from a navigation destination or a row action, where that receiver is gone.

From 2.3, nucleusApplication provides LocalNucleusApplicationScope on both backends (and bridges parent locals into each Tao scene). Receiver-less overloads read it:

import androidx.compose.runtime.Composable
import dev.nucleusframework.application.DecoratedWindow
import dev.nucleusframework.application.LocalNucleusApplicationScope

@Composable
fun EditorWindow(onClose: () -> Unit) {
    // No application receiver — reads LocalNucleusApplicationScope
    DecoratedWindow(onCloseRequest = onClose, title = "Editor") {
        EditorContent()
    }
}

@Composable
fun MaterialEditorWindow(onClose: () -> Unit) {
    // Toolkit wrappers still need the scope as a receiver:
    with(LocalNucleusApplicationScope.current) {
        MaterialDecoratedWindow(onCloseRequest = onClose) {
            EditorContent()
        }
    }
}

Outside nucleusApplication { }, reading the local throws. Prefer the receiver-less DecoratedWindow / DecoratedDialog when you only need the core APIs; use LocalNucleusApplicationScope.current explicitly for toolkit wrappers (MaterialDecoratedWindow, Jewel, Fluent, …) that remain scope extensions.

From 2.3.2, each secondary Tao scene also re-provides LocalTaoWindow and LocalTitleBarInfo for that window after parent locals are bridged in. Title-bar drag, double-click maximize, and system controls therefore target the child window, not the parent that opened it. BasicTitleBar binds drag with Modifier.windowDragArea(window) so it stays correct even if a parent LocalTaoWindow is still visible in the composition tree.

Hosted windows

Libraries and navigation layers should not hard-code Compose Desktop's AWT androidx.compose.ui.window.Window / Dialog (unsupported under Tao) or a specific design-system window type. From 2.3.2, nucleusApplication also provides LocalNucleusWindowHost and LocalNucleusDialogHost. The defaults open a plain DecoratedWindow / DecoratedDialog, and take the Tao knobs popupFor, nativePopupLayers, nativeContextMenu, hiddenFromDock, and alwaysOnBottom. The remaining overlay flags — transparent, clickThrough, visibleOnAllWorkspaces, and forceX11 — are only on DecoratedWindow itself. Call sites use HostedWindow / HostedDialog:

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberWindowState
import dev.nucleusframework.application.HostedWindow
import dev.nucleusframework.application.LocalNucleusWindowHost
import dev.nucleusframework.window.TitleBar

@Composable
fun DeepSearchDestination(onBack: () -> Unit) {
    HostedWindow(
        onCloseRequest = onBack,
        state = rememberWindowState(size = DpSize(1200.dp, 800.dp)),
        title = "Deep search",
    ) {
        TitleBar { /* … */ }
        DeepSearchContent()
    }
}

// App chrome: one override, every HostedWindow call site picks it up
@Composable
fun AppRoot() {
    CompositionLocalProvider(LocalNucleusWindowHost provides myMaterialHost) {
        NavigationGraph()
    }
}

Override the host when the app wraps every secondary window in Material, Jewel, or custom chrome without teaching every library or navigation destination about that wrapper. Direct DecoratedWindow / DecoratedDialog calls remain valid when you want the concrete type.

How it works

nucleusApplication { } resolves the backend and exposes DecoratedWindow on NucleusApplicationScope. On Tao, that overload delegates to dev.nucleusframework.window.tao.ApplicationScope.DecoratedWindow, which opens a TaoWindow and mounts a ComposeScene against the native surface.

Inside the content lambda you get a NucleusDecoratedWindowScope. Its nucleusWindow property returns the backend-agnostic NucleusWindow handle: focus state, minimized / maximized / fullscreen flows, icon, and minimum size. Reach the raw TaoWindow through nucleusWindow.unsafe.taoWindow when you need Tao-only behavior.

Title-bar styling is shared with the AWT backends. TitleBarStyle, TitleBarColors, and TitleBarMetrics live in decorated-window-core, and NucleusDecoratedWindowTheme provides them through composition locals. The same theme works whether the dependency is -tao, -jbr, or -jni.

API reference

DecoratedWindow parameters

ParameterTypeNotes
onCloseRequest() -> UnitFired by the OS close affordance.
stateWindowStatePosition, size, placement. An axis set to Dp.Unspecified wraps to content after the first composition.
visibleBooleanDefault true.
titleStringOS-level window title.
iconPainter?Taskbar / dock icon.
resizableBooleanDefault true.
enabled / focusable / alwaysOnTopBooleanStandard window flags.
undecoratedBooleanBorderless window with no controls. Honored by Tao; ignored by AWT.
isDialogBooleanDefault false. Open as a dialog-class window.
transparentBooleanDefault false. Per-pixel window transparency. Creation-time only. See Overlay windows.
clickThroughBooleanDefault false. Pointer events pass through to whatever is behind. Reactive.
alwaysOnBottomBooleanDefault false. Keep the window below normal windows. Clears alwaysOnTop. Reactive.
visibleOnAllWorkspacesBooleanDefault false. Show on every workspace or Space. Reactive.
forceX11BooleanDefault false. Linux only: take an X11 (XWayland) surface for this window alone. Creation-time only.
nativeContextMenuBooleanDefault false. Replace Compose context menus with the platform's. On the backend-agnostic DecoratedWindow and HostedWindow, not the Tao-scope overload. See Context menus.
macOSStyleMacOSStyleDefault MacOSStyle.Classic. Title-bar treatment on macOS.
compositionLocalContextCompositionLocalContext?Composition locals to carry into the new window's composition.
popupForNucleusWindow?Linux/Tao only: attach this window as a popup overlay of another. On native Wayland, positions are in the parent content area (hidden-titlebar CSD origin is applied inside setOuterPosition).
nativePopupLayersBooleanDefault false. Materialise Compose Popup layers as native transparent windows (NSPanel / WS_POPUP HWND). Tao only.
hiddenFromDockBooleanDefault false. Hide this window from the OS taskbar/Dock. Honored by Tao; ignored by AWT.
minimumSizeDpSize?Enforced after the first layout pass.
onPreviewKeyEvent / onKeyEvent(KeyEvent) -> BooleanReturn true to consume the event.
content@Composable NucleusDecoratedWindowScope.() -> UnitTitle bar slot plus your UI.

The table above lists the Tao ApplicationScope.DecoratedWindow overload. The backend-agnostic NucleusApplicationScope.DecoratedWindow in nucleus-application takes the same window flags — including transparent, clickThrough, alwaysOnBottom, visibleOnAllWorkspaces, and forceX11 — and adds nativeContextMenu. It does not expose macOSStyle, isDialog, or compositionLocalContext.

Flags that only the Tao backend can honor are accepted and ignored on AWT, so the same source compiles and runs on either backend.

Hiding from the taskbar/Dock

hiddenFromDock keeps the window visible and focusable while removing it from the OS-level window list — useful for HUDs, overlays, and background utility windows that should not clutter the taskbar or app switcher:

DecoratedWindow(
    onCloseRequest = ::exitApplication,
    hiddenFromDock = true,
) {
    // ...
}

The mechanism differs per platform:

  • macOS — switches the shared NSApplication to the accessory activation policy (no Dock icon, no menu bar). This is app-wide, not per-window: the last window to apply the flag wins.
  • Windows — sets WS_EX_TOOLWINDOW on the window, which drops its taskbar button and its Alt+Tab entry. Per-window.
  • Linux — sets the GTK skip-taskbar/skip-pager hints (_NET_WM_STATE_SKIP_TASKBAR). Per-window, and only effective on X11 or XWayland.

On native Wayland, hiddenFromDock has no effect — Wayland has no client-side skip-taskbar protocol (xdg-shell, gtk_shell1, and the staging layer-shell extensions all lack it, and Mutter rejects wlr-layer-shell). Nucleus logs a warning rather than failing silently. Force XWayland with NUCLEUS_TAO_LINUX_RENDERER=x11 if you need the window actually hidden on a Wayland session — see Native Wayland.

For a menu-bar or tray app that should stay out of the Dock until it opens a real window, pass dockIconFollowsWindows = true to nucleusApplication instead of setting LSUIElement in Info.plist. The app starts as an accessory; a Dock tile appears only while at least one DecoratedWindow with hiddenFromDock = false is visible. Standalone tray popups never count. The flag is ignored off macOS and on the AWT backend. See Hide the Dock icon on macOS.

Scope members

interface NucleusDecoratedWindowScope : DecoratedWindowScope {
    val nucleusWindow: NucleusWindow
}

interface NucleusWindow {
    val isFocused: Boolean
    val isMinimized: Boolean
    val isMaximized: Boolean
    val isFullscreen: Boolean
    val focusFlow: StateFlow<Boolean>
    fun setMaximized(maximized: Boolean)
    fun setFullscreen(fullscreen: Boolean)
    fun setMinimumSize(size: DpSize?)
    fun setIcon(painter: Painter?)
    fun close()
    val unsafe: NucleusWindowUnsafe   // .taoWindow, .taoHandle
}

Title bar modifiers

  • Modifier.macOSLargeCornerRadius() — opt into macOS 26+ rounded corners.
  • Modifier.newFullscreenControls() — relayout the title bar buttons for fullscreen.

Scaffold and chrome (2.2+)

Prefer these when the stock TitleBar layout is too rigid:

  • WindowScaffold / TitleBarPlacement — full-window and overlay chrome layouts.
  • Modifier.windowDragArea / noWindowDrag — declare (or exclude) native move regions.
  • WindowControls — system min/max/close outside TitleBar.
  • WindowBackground / WindowAppearance — clear colour and native light/dark from inside the tree.
  • Modifier.windowGlassRegion (macOS) / WindowsBackdrop (Windows 11) — platform materials.

Full reference: Window scaffold and chrome.

TitleBar is a single extension on DecoratedWindowScope. The nucleusApplication scope (NucleusDecoratedWindowScope) and the Tao-native taoApplication scope (TaoDecoratedWindowScope) both implement it, so the same TitleBar call works whichever entry point you start from. The same holds for WindowScaffold and the chrome primitives.

Text input

IME composition (Japanese, Chinese, Korean, and similar) is forwarded into Compose as preedit on macOS, Windows, and Linux. Keys the IME already consumed — conversion Enter, candidate arrows, Backspace on the preedit — are dropped so they do not also insert a newline, move the caret, or delete committed text. The candidate window follows the caret on all three platforms.

Linux resolves the platform input method the way GTK's own text widgets do: ibus or fcitx5 through the GTK immodule on X11, and the text-input-v3 client on Wayland. Linux preedit requires 2.5.6 or later — earlier releases pinned GTK's built-in fallback context, which never reaches a system input method.

On macOS, whether a held letter repeats or opens the accent picker is left entirely to the operating system: Nucleus neither reads nor writes ApplePressAndHoldEnabled. A pick is applied from the replacementRange AppKit sends with it, so it replaces the base letter without touching the text around it (2.5.6; earlier releases forced the user default and inferred the picker state).

Handle uncaught exceptions

From 2.5.8, an exception that escapes a Tao event dispatch no longer vanishes at the JNI boundary. The default path logs the stack at SEVERE, shows a blocking native error dialog, exits the Tao loop, and rethrows so nucleusApplication / taoApplication terminate with exit code 1. The dialog is shown after the loop has exited — a modal pump inside a tao callback deadlocks the main thread.

From 2.5.9 the dialog carries the full stack in a bounded scrollable monospace view with a Copy button: an NSAlert run by an out-of-process osascript child on macOS (compact CFUserNotification fallback), an in-memory DLGTEMPLATE on a fresh thread on Windows (MessageBoxW fallback), and a GtkMessageDialog on Linux.

Skip the dialog in unattended runs (CI, AOT training) with -Dnucleus.tao.fatalErrorDialog=false. The SEVERE log remains.

To keep a window alive after a recoverable failure, provide a LocalWindowExceptionHandlerFactory. It is the Tao mirror of Compose Desktop's AWT factory and is @ExperimentalComposeUiApi. Handlers run on the thread the failure occurred on, and cover layout, draw, input, IME, accessibility, and nativePopupLayers popups of that window:

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.WindowExceptionHandler
import dev.nucleusframework.application.DecoratedWindow
import dev.nucleusframework.application.NucleusBackend
import dev.nucleusframework.application.nucleusApplication
import dev.nucleusframework.window.tao.LocalWindowExceptionHandlerFactory
import dev.nucleusframework.window.tao.WindowExceptionHandlerFactory

@OptIn(ExperimentalComposeUiApi::class)
fun main() = nucleusApplication(backend = NucleusBackend.Tao) {
    CompositionLocalProvider(
        LocalWindowExceptionHandlerFactory provides WindowExceptionHandlerFactory { _ ->
            WindowExceptionHandler { throwable ->
                // Return normally to drop the frame and keep the window alive.
                // Rethrow to take the fatal dialog and exit.
            }
        },
    ) {
        DecoratedWindow(onCloseRequest = ::exitApplication, title = "Demo") {
            MyContent()
        }
    }
}

Returning normally is a real resume for layout, draw, input, IME, and accessibility: the offending frame is dropped and a new one is requested. A failure inside composition is not recoverable — Compose stops the recomposition loop before the backend sees the exception, and that loop cannot be restarted. Swallowing one logs at SEVERE instead of leaving a window that silently paints its last frame; close and recreate the window, or rethrow to take the fatal path. DefaultWindowExceptionHandlerFactory always rethrows.

Notes

  • macOS requires -XstartOnFirstThread. The Nucleus Gradle plugin adds it for you.
  • For multi-window apps, call DecoratedWindow several times from the same nucleusApplication block. Each window gets its own NucleusWindow.
  • Linux CSD windows draw a native GTK drop shadow (hidden-titlebar pattern) that tracks the window during interactive moves.
  • Windowed Windows CSD (no Mica/Acrylic) clears the DWM border fill so the Compose frame stroke is visible, and draws the Win11 8px rounded contour through DWM so corners are not clipped. Backdrop windows keep the system frame.
  • NucleusApplicationScope implements Compose's ApplicationScope, so libraries scoped to the plain Compose receiver (for example system-tray composables) resolve inside nucleusApplication { }.
  • From 2.5.14, DecoratedWindow / DecoratedDialog (and the hosted and Tao variants) are @ComposableOpenTarget(-1) with a @UiComposable content lambda. A non-UI-targeted composable called in the nucleusApplication block no longer reclassifies the window content, so @UiComposable calls inside the window stay valid under -Werror.

What's next