Nucleus
Tao backend

Window scaffold and chrome

Build full-window layouts and custom title bars on Tao with WindowScaffold, drag areas, WindowControls, and platform materials.

DecoratedWindow + TitleBar always laid the main content below a fixed chrome band. That blocked full-window layouts: no list under the toolbar, no glass sidebar that meets the traffic lights, no design-system headerbar that still drags the window. From 2.2, WindowScaffold and a small set of chrome primitives solve that without throwing away native caption zones, button order, or drag behaviour.

Add the dependency

The scaffold APIs ship in decorated-window-tao (same artifact as the Tao backend):

build.gradle.kts
dependencies {
    implementation("dev.nucleusframework:nucleus.nucleus-application:2.5.16")
    implementation("dev.nucleusframework:nucleus.decorated-window-tao:2.5.16")
}

Existing DecoratedWindow + TitleBar call sites keep working. The scaffold is additive.

Quickstart

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.nucleusframework.application.DecoratedWindow
import dev.nucleusframework.application.nucleusApplication
import dev.nucleusframework.window.TitleBarPlacement
import dev.nucleusframework.window.WindowBackground
import dev.nucleusframework.window.WindowControls
import dev.nucleusframework.window.WindowScaffold
import dev.nucleusframework.window.windowDragArea

fun main() = nucleusApplication {
    DecoratedWindow(onCloseRequest = ::exitApplication, title = "Scaffold demo") {
        MaterialTheme {
            WindowBackground(MaterialTheme.colorScheme.background)
            WindowScaffold(
                titleBar = {
                    Box(
                        Modifier
                            .fillMaxWidth()
                            .height(48.dp)
                            .windowDragArea()
                            .padding(horizontal = 12.dp),
                    ) {
                        Text("My App", Modifier.align(Alignment.CenterStart))
                        WindowControls(Modifier.align(Alignment.CenterEnd))
                    }
                },
                titleBarPlacement = TitleBarPlacement.Overlay(),
            ) { contentPadding ->
                Box(Modifier.fillMaxSize().padding(contentPadding)) {
                    /* app content — scrolls under the overlay bar when padded */
                }
            }
        }
    }
}

How it works

WindowScaffold is the sole intended child of a DecoratedWindow content lambda. It:

  1. Measures the titleBar slot and publishes the height to the native layer (macOS traffic-light centring, Windows caption zone / touch drag) — replacing the hard-coded TitleBarStyle.metrics.height contract.
  2. Provides LocalWindowChromeInsets so chrome and content avoid platform-reserved control zones (traffic lights, KDE edge padding).
  3. In Overlay mode, lets content fill the full window height behind the bar and hands the measured bar height back as top PaddingValues.

Nothing is implicitly draggable at the scaffold level. Custom chrome must declare its move surface with windowDragArea — the same path the stock TitleBar already uses.

Title bar placement

PlacementBehaviour
TitleBarPlacement.DockedClassic layout: the bar takes height, content fills the rest. Default.
TitleBarPlacement.Overlay(autoHideInFullscreen = true, passThroughToContent = false)Content is fillMaxSize; the bar floats on top. Content receives the bar height as top padding.

controlButtonsDirection sets the order and side of the system buttons for the whole scaffold — ControlButtonsDirection.Auto follows the platform, Rtl moves them to the leading edge and flips the macOS traffic lights.

autoHideInFullscreen drops the bar while fullscreen so content is fully immersive. passThroughToContent hit-tests presses against content under the bar as well, so controls merged into the title-bar band (a collapsed nav pane's hamburger, a tab strip) stay interactive. Off by default: with full-bleed content scrolling behind opaque chrome, a click on the bar would otherwise activate whatever sits underneath.

Chrome primitives

Drag areas

Modifier = Modifier
    .windowDragArea(
        enabled = true, // default; false makes the whole modifier a no-op
        doubleClickAction = WindowDoubleClickAction.ToggleMaximize,
    )

An unconsumed primary press followed by a move starts the native window move. Interactive children (buttons, text fields) opt out by consuming the press. Gesture detectors that only claim the pointer once it moves — scrollbars, sliders, resize handles — leave the press unconsumed, so wrap them:

Scrollbar(Modifier.noWindowDrag())

Prefer Modifier.windowDragArea(window = taoWindow) when chrome already holds a TaoWindow (title bars, secondary scenes). The no-arg form reads LocalTaoWindow; the explicit overload stays correct if parent-window CompositionLocals are bridged into a child scene (stock title bars do this from 2.3.2).

WindowDoubleClickAction.None disables the double-click → maximize gesture on that area.

Window controls

WindowControls draws the system min / maximize-restore / close strip outside TitleBar, so a design system can place it inside its own toolbar:

WindowControls(
    modifier = Modifier.align(Alignment.CenterEnd),
    direction = ControlButtonsDirection.Auto,   // default
    renderer = WindowControlsRenderer.Platform, // default
)

Nucleus owns the semantics (maximize ↔ restore; fullscreen swap; close → onCloseRequest). The renderer only draws. direction decides the button order and side: Auto follows the platform — including the desktop's own button-layout on Linux — and Rtl moves them to the leading edge, which on macOS also flips the AppKit traffic lights. On macOS, WindowControlsRenderer.Platform draws nothing and reserves the traffic-light footprint instead — pad with either WindowControls or LocalWindowChromeInsets.controlsInsets, never both. In native fullscreen the replacement traffic lights cannot miniaturize the window, so the minimize button is disabled and no longer looks clickable; close and zoom stay on the same hover path.

Supply a custom WindowControlsRenderer to draw buttons in the design system's own style.

Chrome insets

val insets = LocalWindowChromeInsets.current
Row(Modifier.padding(insets.controlsInsets)) { /* title bar content */ }

controlsInsets is the horizontal safe area for system controls; titleBarHeight is the measured slot height (0.dp when the bar is hidden).

Platform materials and appearance

Window background

Tao gives every window its own ComposeScene, so the theme often lives inside the window. WindowBackground sets the clear colour from inside that tree — the colour that shows during live resize, around floating panels, and wherever Compose paints nothing:

WindowBackground(MaterialTheme.colorScheme.background)

Window appearance

Force native surfaces (glass, traffic lights, menus, Windows chrome glyphs) to follow the app theme instead of the OS setting:

WindowAppearance(
    if (dark) WindowAppearanceMode.Dark else WindowAppearanceMode.Light,
)

A no-op on Linux. Reverts to system-derived appearance when the composable leaves composition.

macOS glass regions

Modifier.windowGlassRegion hosts a real NSSplitViewController pane under the Compose surface so AppKit applies the wallpaper-tinted System Settings material — desktop shows through, intervening windows never do:

Box(
    Modifier
        .width(248.dp)
        .fillMaxHeight()
        .windowGlassRegion(WindowGlassRegionKind.Sidebar, cornerRadius = 12.dp),
) { /* sidebar content; leave unpainted areas for the material */ }

Kinds: Sidebar, ContentList, Inspector (macOS 14+). The component must not paint a full opaque background. macOS only; a no-op elsewhere. There is no full-window macOS glass — that is not an AppKit pattern.

Windows 11 backdrops

WindowsBackdrop(
    style = WindowsBackdropStyle.Mica,       // or Acrylic, MicaAlt
    tint = Color.Unspecified,                // app tint over the material
    tier = WindowsBackdropTier.Auto,         // Modern / LegacyMica / Windows10Acrylic
)

DWM composites the material behind the window wherever the app paints nothing. Leave panels or margins unpainted to reveal it; a full-bleed opaque background covers it. WindowBackground is suspended while a backdrop is active and restored when it leaves composition.

Degrades on older Windows: pre-22H2 Windows 11 maps Mica styles to the older single-material attribute; Windows 10 falls back to acrylic when available. Silent no-op on macOS and Linux.

Transparent and borderless overlays

Full-window transparency is a creation-time flag, implemented by the Tao backend and accepted and ignored on AWT. It is available on the backend-agnostic DecoratedWindow, on the Tao-scope overload, and on the Material and Jewel window wrappers:

import dev.nucleusframework.application.DecoratedWindow
import dev.nucleusframework.application.nucleusApplication

fun main() = nucleusApplication {
    DecoratedWindow(
        onCloseRequest = ::exitApplication,
        undecorated = true,   // no frame, no DWM shadow
        transparent = true,   // empty client composites the desktop
    ) {
        Box(Modifier.size(48.dp).background(Color(0xFFFF00AA)))
    }
}

Hosts clear to alpha-0; opaque WindowBackground / title-bar clears are coerced to transparent so empty regions stay see-through. Semi-transparent tints still apply.

For click-through, always-on-bottom, and all-workspaces overlays built on this flag, see Overlay windows.

Notes

  • Intended as the sole child of the window content lambda; it fills the remaining window height.
  • Stock TitleBar still works for apps that do not need full-window layout — no migration required.
  • passThroughToContent is opt-in for a reason: enable it only when interactive content is meant to live under the chrome band.

What's next