Skip to content

Decorated Window

Compose for Desktop does not allow drawing custom content in the title bar while keeping native window controls and native behavior (drag, resize, double-click maximize). You must choose between a native title bar you cannot customize, or a fully undecorated window where you reimplement everything from scratch.

The decorated window modules bridge this gap. They are a fork of Jewel's decorated window, without any dependency on Jewel itself. Key differences from Jewel:

  • No JNA — all native calls use JNI only, removing the JNA dependency entirely
  • Design-system agnostic — no Material dependency; easily map any theme (Material 3, Jewel, your own) to its styling tokens
  • DecoratedDialog — custom title bar for dialog windows, which Jewel does not provide
  • Reworked Linux rendering — the entire Linux experience has been rebuilt from the ground up to look as native as possible, even though everything is drawn with Compose: platform-accurate GNOME Adwaita and KDE Breeze window controls, proper window shape clipping, border styling, and full behavior emulation (drag, double-click maximize, focus-aware button states)

Module Structure

The decorated window functionality is split into three modules:

Module Artifact Description
decorated-window-core nucleus.decorated-window-core Shared types, layout, styling, resources. No platform-specific code.
decorated-window-jbr nucleus.decorated-window-jbr JBR-based implementation. Uses JetBrains Runtime's CustomTitleBar API on macOS and Windows.
decorated-window-jni nucleus.decorated-window-jni JBR-free implementation. Uses JNI native libraries on all platforms, with pure-Compose fallbacks when native libs are unavailable.

Both decorated-window-jbr and decorated-window-jni expose the same public API. Choose the one that fits your runtime:

decorated-window-jbr — JetBrains Runtime implementation

Uses JetBrains' official CustomTitleBar API. This is the more battle-tested option, backed by the same code that powers IntelliJ IDEA and other JetBrains products. Requires JBR.

However, there are a few known issues on Windows:

  • The window cannot open in maximized state directly — you need to use a LaunchedEffect with a short delay after the window appears, then set WindowPlacement.Maximized
  • Title bar drag events are occasionally missed, causing the window to not follow the cursor during drag

These are upstream JBR bugs, not Nucleus bugs. The module throws an IllegalStateException at startup if JBR is not detected.

Tip

When running via ./gradlew run, Gradle uses the JDK configured in your toolchain. Make sure it is a JBR distribution if using this module.

decorated-window-jni — Nucleus native implementation

Entirely implemented by Nucleus using JNI native libraries on all platforms. None of the JBR bugs mentioned above are present — window maximization and drag work reliably.

This module does not depend on JBR, making it compatible with any JVM (OpenJDK, GraalVM Native Image, etc.). It was specifically designed for use cases where JBR is not available, such as GraalVM native-image builds. On Linux, pair it with linux-hidpi for correct HiDPI support.

Less battle-tested

While the JNI module has no known bugs, it has not been as widely tested as the JBR implementation. Use it with appropriate caution in production, and report any issues you encounter.

Installation

Choose one implementation:

dependencies {
    // Option 1: JBR-based (requires JetBrains Runtime)
    implementation("io.github.kdroidfilter:nucleus.decorated-window-jbr:<version>")

    // Option 2: JNI-based (works on any JVM)
    implementation("io.github.kdroidfilter:nucleus.decorated-window-jni:<version>")
}

If you use Material 3, add the companion module:

dependencies {
    implementation("io.github.kdroidfilter:nucleus.decorated-window-material:<version>")
}

Note

See decorated-window-material for automatic MaterialTheme.colorScheme wiring.

Quick Start

fun main() = application {
    NucleusDecoratedWindowTheme(isDark = true) {
        DecoratedWindow(
            onCloseRequest = ::exitApplication,
            title = "My App",
        ) {
            TitleBar { state ->
                Text(
                    title,
                    modifier = Modifier.align(Alignment.CenterHorizontally),
                    color = LocalContentColor.current,
                )
            }
            // Your app content
            MyContent()
        }
    }
}

Screenshots

macOS Decorated Window

Windows Decorated Window

GNOME Decorated Window

KDE Decorated Window

Platform Behavior

JBR module (decorated-window-jbr)

macOS Windows Linux
Decoration JBR CustomTitleBar JBR CustomTitleBar Fully undecorated
Window controls Native traffic lights Native min/max/close Compose WindowControlArea (SVG icons)
Drag JBR hit-test JBR forceHitTest JBR.getWindowMove().startMovingTogetherWithMouse()
Double-click maximize Native Native Manual detection
RTL support No (requires custom JBR for hot-swap) Yes (no hot-swap, restart required) Yes (hot-swap)

JNI module (decorated-window-jni)

macOS Windows Linux
Decoration JNI native bridge JNI DLL (WndProc subclass) JNI .so (_NET_WM_MOVERESIZE)
Window controls Native traffic lights Compose WindowsWindowControlArea (SVG icons) Compose WindowControlArea (SVG icons)
Drag nativeStartWindowDrag() via JNI Native DLL or Compose fallback _NET_WM_MOVERESIZE or Compose fallback
Double-click maximize Native via JNI Native or Compose detection Compose detection
Fallback (no native lib) AWT client properties Compose windowDragHandler() Compose windowDragHandler()
RTL support Yes (live hot-swap) Yes (live hot-swap) Yes (hot-swap)

On macOS, both modules preserve the native traffic lights.

On Windows, the JBR module uses the native min/max/close buttons, while the JNI module draws its own window controls with Compose (SVG icons matching the Windows style).

On Linux, the window is fully undecorated in both modules. They render their own close/minimize/maximize buttons using SVG icons adapted to the desktop environment (GNOME Adwaita or KDE Breeze). The window shape is also clipped to rounded corners to match the native look.

Components

NucleusDecoratedWindowTheme

Provides styling for all decorated window components via CompositionLocal. Must wrap DecoratedWindow / DecoratedDialog.

NucleusDecoratedWindowTheme(
    isDark: Boolean = true,
    windowStyle: DecoratedWindowStyle = DecoratedWindowDefaults.dark/lightWindowStyle(),
    titleBarStyle: TitleBarStyle = DecoratedWindowDefaults.dark/lightTitleBarStyle(),
) {
    // DecoratedWindow / DecoratedDialog go here
}

The isDark flag selects the built-in dark or light presets. Pass your own windowStyle / titleBarStyle to override any or all values.

DecoratedWindow

Drop-in replacement for Compose Window(). Manages window state (active, fullscreen, minimized, maximized) and platform-specific decorations.

DecoratedWindow(
    onCloseRequest = ::exitApplication,
    state = rememberWindowState(),
    title = "My App",
    icon = null,
    resizable = true,
) {
    TitleBar { state -> /* title bar content */ }
    // window content
}

The content lambda receives a DecoratedWindowScope which exposes:

  • window: ComposeWindow — the underlying AWT window
  • state: DecoratedWindowState — current window state (.isActive, .isFullscreen, .isMinimized, .isMaximized)

DecoratedDialog

Same concept for dialog windows. Uses DialogWindow internally. Dialogs only show a close button on Linux (no minimize/maximize).

DecoratedDialog(
    onCloseRequest = { showDialog = false },
    title = "Settings",
    resizable = false,
) {
    DialogTitleBar { state ->
        Text(title, modifier = Modifier.align(Alignment.CenterHorizontally))
    }
    // dialog content
}

TitleBar / DialogTitleBar

Platform-dispatched title bar composable. Provides a TitleBarScope with:

  • title: String — the window title passed to DecoratedWindow
  • icon: Painter? — the window icon
  • Modifier.align(alignment: Alignment.Horizontal) — positions content within the title bar
TitleBar { state ->
    // Left-aligned icon
    Icon(
        painter = myIcon,
        contentDescription = null,
        modifier = Modifier.align(Alignment.Start),
    )

    // Centered title
    Text(
        title,
        modifier = Modifier.align(Alignment.CenterHorizontally),
        color = LocalContentColor.current,
    )

    // Right-aligned action
    IconButton(
        onClick = { /* ... */ },
        modifier = Modifier.align(Alignment.End),
    ) {
        Icon(Icons.Default.Settings, contentDescription = "Settings")
    }
}

Centered content is automatically shifted to avoid overlapping with start/end content.

Styling

Mapping Your Own Theme

The key idea: NucleusDecoratedWindowTheme accepts two style objects. You build them from whatever color system you use:

// Example: map a custom theme to decorated window styles
val myWindowStyle = DecoratedWindowStyle(
    colors = DecoratedWindowColors(
        border = MyTheme.colors.border,
        borderInactive = MyTheme.colors.border.copy(alpha = 0.5f),
    ),
    metrics = DecoratedWindowMetrics(borderWidth = 1.dp),
)

val myTitleBarStyle = TitleBarStyle(
    colors = TitleBarColors(
        background = MyTheme.colors.surface,
        inactiveBackground = MyTheme.colors.surfaceDim,
        content = MyTheme.colors.onSurface,
        border = MyTheme.colors.outline,
    ),
    metrics = TitleBarMetrics(height = 40.dp),
    icons = TitleBarIcons(), // null = use platform defaults
)

NucleusDecoratedWindowTheme(
    isDark = MyTheme.isDark,
    windowStyle = myWindowStyle,
    titleBarStyle = myTitleBarStyle,
) {
    DecoratedWindow(...) { ... }
}

DecoratedWindowStyle

Controls the window border (visible only on Linux):

Property Description
colors.border Border color when window is active
colors.borderInactive Border color when window is inactive
metrics.borderWidth Border width (default 1.dp)

TitleBarStyle

Controls the title bar appearance:

Property Description
colors.background Title bar background when active
colors.inactiveBackground Title bar background when inactive
colors.content Default content color (exposed via LocalContentColor)
colors.border Bottom border of the title bar
colors.fullscreenControlButtonsBackground Background for macOS fullscreen traffic lights
colors.iconButtonHoveredBackground Background for icon buttons on hover
colors.iconButtonPressedBackground Background for icon buttons on press
metrics.height Title bar height (default 40.dp)
metrics.gradientStartX / gradientEndX Gradient range (see below)
icons Custom Painter for close/minimize/maximize/restore buttons (null = platform default)

Gradient

The TitleBar composable accepts a gradientStartColor parameter. When set, the title bar background becomes a horizontal gradient that transitions from the background color through gradientStartColor and back to background:

[background] → [gradientStartColor] → [background]

The gradient range is controlled by TitleBarMetrics.gradientStartX and gradientEndX.

TitleBar(
    gradientStartColor = Color(0xFF6200EE),
) { state ->
    Text(title, modifier = Modifier.align(Alignment.CenterHorizontally))
}

When gradientStartColor is Color.Unspecified (the default), the background is a solid color.

macOS Fullscreen Controls

On macOS, use the newFullscreenControls() modifier on TitleBar to enable the new-style fullscreen controls (traffic lights stay visible in fullscreen mode with a colored background):

TitleBar(modifier = Modifier.newFullscreenControls()) { state ->
    // ...
}

With decorated-window-jbr, this sets the apple.awt.newFullScreenControls system property and uses fullscreenControlButtonsBackground from your TitleBarStyle.

With decorated-window-jni, fullscreen button management is handled natively — the modifier is a no-op but safe to call.

ProGuard

Both modules use JNI on macOS. When ProGuard is enabled, the native bridge classes must be preserved. The Nucleus Gradle plugin includes these rules automatically, but if you need to add them manually:

# Nucleus decorated-window-jbr JNI
-keep class io.github.kdroidfilter.nucleus.window.utils.macos.NativeMacBridge {
    native <methods>;
}

# Nucleus decorated-window-jni JNI (all platforms)
-keep class io.github.kdroidfilter.nucleus.window.utils.macos.JniMacTitleBarBridge {
    native <methods>;
}
-keep class io.github.kdroidfilter.nucleus.window.utils.windows.JniWindowsDecorationBridge {
    native <methods>;
}
-keep class io.github.kdroidfilter.nucleus.window.utils.linux.JniLinuxWindowBridge {
    native <methods>;
}

-keep class io.github.kdroidfilter.nucleus.window.** { *; }

RTL (Right-to-Left) Layout Support

Windows

Both modules support RTL layout on Windows, but they differ in how they handle runtime direction changes:

  • decorated-window-jbr: Supports RTL layout, but does not support hot-swapping between RTL and LTR at runtime. If your application needs to switch layout direction, the user must restart the application for the change to take effect.
  • decorated-window-jni: Supports RTL layout with live hot-swapping — the title bar and window controls update immediately when the layout direction changes at runtime, with no restart required.

macOS

The standard JetBrains Runtime does not support RTL for the title bar on macOS — the traffic lights and title bar layout always remain in LTR mode.

Two options are available for RTL support on macOS:

  • decorated-window-jni: Fully supports RTL layout with live hot-swapping, no custom JDK required.
  • decorated-window-jbr with the custom JBR fork (v25.0.2b329.66-rtl): Supports RTL layout with live hot-swapping as well. This fork includes a native RTL fix for macOS window decorations.

Linux

RTL layout is handled entirely by Compose since the window is fully undecorated on Linux. Both modules support RTL with live hot-swapping.

Linux Desktop Environment Detection

On Linux, the module detects the current desktop environment and loads the appropriate icon set:

  • GNOME — Adwaita-style icons, rounded top corners (12dp radius)
  • KDE — Breeze-style icons, rounded top corners (5dp radius)
  • Other — Falls back to GNOME style

Detection uses XDG_CURRENT_DESKTOP and DESKTOP_SESSION environment variables.