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
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
| Parameter | Type | Notes |
|---|---|---|
onCloseRequest | () -> Unit | Fired by the OS close affordance. |
state | WindowState | Position, size, placement. An axis set to Dp.Unspecified wraps to content after the first composition. |
visible | Boolean | Default true. |
title | String | OS-level window title. |
icon | Painter? | Taskbar / dock icon. |
resizable | Boolean | Default true. |
enabled / focusable / alwaysOnTop | Boolean | Standard window flags. |
undecorated | Boolean | Borderless window with no controls. Honored by Tao; ignored by AWT. |
isDialog | Boolean | Default false. Open as a dialog-class window. |
transparent | Boolean | Default false. Per-pixel window transparency. Creation-time only. See Overlay windows. |
clickThrough | Boolean | Default false. Pointer events pass through to whatever is behind. Reactive. |
alwaysOnBottom | Boolean | Default false. Keep the window below normal windows. Clears alwaysOnTop. Reactive. |
visibleOnAllWorkspaces | Boolean | Default false. Show on every workspace or Space. Reactive. |
forceX11 | Boolean | Default false. Linux only: take an X11 (XWayland) surface for this window alone. Creation-time only. |
nativeContextMenu | Boolean | Default false. Replace Compose context menus with the platform's. On the backend-agnostic DecoratedWindow and HostedWindow, not the Tao-scope overload. See Context menus. |
macOSStyle | MacOSStyle | Default MacOSStyle.Classic. Title-bar treatment on macOS. |
compositionLocalContext | CompositionLocalContext? | Composition locals to carry into the new window's composition. |
popupFor | NucleusWindow? | 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). |
nativePopupLayers | Boolean | Default false. Materialise Compose Popup layers as native transparent windows (NSPanel / WS_POPUP HWND). Tao only. |
hiddenFromDock | Boolean | Default false. Hide this window from the OS taskbar/Dock. Honored by Tao; ignored by AWT. |
minimumSize | DpSize? | Enforced after the first layout pass. |
onPreviewKeyEvent / onKeyEvent | (KeyEvent) -> Boolean | Return true to consume the event. |
content | @Composable NucleusDecoratedWindowScope.() -> Unit | Title 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
NSApplicationto 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_TOOLWINDOWon 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 outsideTitleBar.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
DecoratedWindowseveral times from the samenucleusApplicationblock. Each window gets its ownNucleusWindow. - 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.
NucleusApplicationScopeimplements Compose'sApplicationScope, so libraries scoped to the plain Compose receiver (for example system-tray composables) resolve insidenucleusApplication { }.- From 2.5.14,
DecoratedWindow/DecoratedDialog(and the hosted and Tao variants) are@ComposableOpenTarget(-1)with a@UiComposablecontent lambda. A non-UI-targeted composable called in thenucleusApplicationblock no longer reclassifies the window content, so@UiComposablecalls inside the window stay valid under-Werror.
What's next
- Window scaffold and chrome — full-window layouts and materials.
- The Tao backend — what the no-AWT backend covers.
- DecoratedWindow across backends — the shared window API.
- Multi-touch and trackpad gestures — read Tao input events.
- Backends — how Nucleus picks Tao or AWT.