# Nucleus — full documentation > Nucleus is the production desktop layer for Compose Multiplatform — native OS integration and packaging for Kotlin teams. Source: https://nucleusframework.dev/en/docs --- # Welcome to Nucleus 2.5 URL: https://nucleusframework.dev/en/docs Nucleus is the production desktop layer for Compose Multiplatform — native OS integration and packaging for Kotlin teams. Nucleus is the production desktop layer for Compose Multiplatform apps written in Kotlin. Compose draws the UI; Nucleus adds native window decorations, operating-system integration, code signing, installers, and store pipelines. You configure the whole build through a single Gradle DSL, and the runtime runs either as a GraalVM native image or on the JVM. If you already ship Android or KMP, Nucleus is how the same team ships desktop — without a second stack. Paste one prompt into your coding agent (Claude Code, Cursor, Codex, …) — it scans your app, asks the right questions, and migrates it to Nucleus phase by phase with verification checkpoints. Zero install: the whole playbook lives at one URL. See [Migrate with an AI agent](/docs/migrate/ai-agent). ## Features - **One language across platforms.** Share Kotlin code with your Android, iOS, and web apps, with Compose UI on top. - **Native windowing (Tao).** Wayland support, multi-touch, pen input, [window scaffold](/docs/tao/window-scaffold), [TextureView](/docs/tao/texture-view) for external GPU frames, and [GPU render context](/docs/tao/gpu-render-context) for in-process renderers. - **Two runtimes, one DSL.** Run as a GraalVM Native Image, or on the JVM with an AOT cache (JDK 25+). - **Over 40 OS integration modules.** Notifications, system tray, dock menus, dark mode, global hotkeys, and taskbar integration, each added as an opt-in dependency. - **18 packaging formats.** DMG, PKG, MSI, AppX, DEB, RPM, AppImage, Snap, and Flatpak among them, with code signing, notarization, and [differential auto-update](/docs/packaging/auto-update#differential-downloads). - **Native design systems.** [macOS 26](https://github.com/NucleusFramework/compose-macos-26-ui), [Fluent](https://github.com/NucleusFramework/compose-fluent-ui), and [Yaru](https://github.com/NucleusFramework/yaru-compose-ui) on Maven Central, plus Jewel and Material adapters in this repo. See [toolkits](/docs/window/toolkits). To build your first app, follow the [quickstart](/docs/start/quickstart). ## What's new in 2.5 - **Spell check.** The OS spell engine in Compose text fields — Hunspell, `NSSpellChecker`, Windows Spell Checking — with suggestions in the context menu. See [Spell check](/docs/os/spell-check#check-a-text-field). - **Native context menus.** One flag swaps the Compose menu for the platform's: `NSMenu`, Fluent, Adwaita, or Breeze. See [Context menus](/docs/window/context-menu#turn-on-the-native-menu). - **Overlay windows.** Click-through, always-on-bottom, and all-workspaces flags for watermarks and desktop widgets. See [Overlay windows](/docs/tao/overlay-windows#build-a-click-through-watermark). - **Compose over native views.** The `NativeView` overlay slot now draws in the window's own scene on all three platforms. See [Overlay Compose content](/docs/tao/native-views#overlay-compose-content). - **Compose 1.12** is required from this line on. See [Compatibility](/docs/start/install#compatibility). Full list: [Changelog](/docs/changelog). ## Talk with us The Nucleus channel is `#nucleus` on the official Kotlin Slack. Use it for questions, design discussion, and bug reports. If you are not on the Kotlin Slack yet, [request an invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up), then open [#nucleus](https://kotlinlang.slack.com/archives/C0BQYKHBGR0). ## What's next - [Why Nucleus](/docs/why-nucleus) — what vanilla Compose Desktop leaves open, and what Nucleus fills. - [Nucleus vs vanilla CMP](/docs/compare/cmp-default) — the decision table for Kotlin teams. - [Migrate with an AI agent](/docs/migrate/ai-agent) — one prompt, zero install. - [Migrate from JetBrains Compose Desktop](/docs/migrate/from-jb-compose) — drop-in migration. - [Quickstart](/docs/start/quickstart) — build and run your first Nucleus desktop app. - [Architecture](/docs/concepts/architecture) — the layered model behind the framework. --- # Why Nucleus URL: https://nucleusframework.dev/en/docs/why-nucleus What Nucleus adds on top of Compose Multiplatform for Kotlin teams shipping real desktop apps. Kotlin and Compose already own Android. They reach iOS and the web. On the desktop, Compose Multiplatform renders the UI — but shipping a real product still means native chrome, OS integration, packaging, signing, auto-update, and store pipelines that the default plugin does not provide. Nucleus is that production layer. Same Compose. Same Kotlin team. The rest of the desktop stack, already built. ## The gap Compose Desktop leaves open Compose Multiplatform is the right UI foundation. It is not a full desktop product stack. | | Vanilla Compose Desktop | Nucleus | |---|---|---| | **UI** | Compose + Skia | Same Compose + Skia | | **Window chrome** | Basic / Material-looking | Native decorations (Liquid Glass, Fluent, Yaru, Jewel) | | **OS APIs** | Thin (`Tray`, `Notification`) | 40+ Kotlin modules (tray, dock, hotkeys, media, dark mode, …) | | **Packaging** | 6 jpackage formats | **18** formats, including store channels | | **Auto-update** | None | Built-in (`updater-runtime`) | | **Signing / CI** | DIY | Signing, notarization, reusable GitHub Actions | | **Runtimes** | JVM | GraalVM Native Image **or** JVM + AOT cache | | **Migration** | — | Drop-in on the JetBrains Compose plugin | If you only need a window and a DMG/MSI/DEB, vanilla CMP is enough. The moment you need native look-and-feel, store distribution, auto-update, or real OS integration without writing JNI per platform — that is Nucleus. Deep dive: [Nucleus vs vanilla Compose Multiplatform](/docs/compare/cmp-default). ## Built for teams that already write Kotlin Nucleus is not a new language bet. It is for teams that already ship Android (or KMP) and need a desktop surface without hiring a second stack. - **Same hiring pool** — Compose skills transfer. No Rust/JS/Electron team required. - **Same codebase path** — domain, networking, and much of the UI live in `commonMain`. - **Same Gradle world** — one DSL, hot reload, familiar CI. - **Drop-in migration** — rename `compose.desktop.application` → `nucleus.application` and keep shipping. See [Migrating from JetBrains Compose Desktop](/docs/migrate/from-jb-compose). ## What Nucleus provides - **Native decorated window** — a `DecoratedWindow` that follows macOS/Windows/Linux title-bar conventions, backed by the [Tao backend](/docs/concepts/backends). - **Runtime modules** — more than 40 modules covering notifications, system tray, global hotkeys, taskbar progress, dark mode, accent colour, dock menus, jump lists, media controls, auto-launch, scheduler, auto-update, system info, and file dialogs. Each is optional and written in Kotlin. - **Two runtimes** — GraalVM Native Image produces a self-contained binary (around 40 MB as an NSIS installer with maximum compression, starting in about 0.2 s); JVM with an AOT cache runs on HotSpot for long-running workloads. Reachability metadata — for your third-party libraries too — is resolved automatically at build time. Switch with one Gradle setting. - **18 packaging formats** — DMG, PKG, Exe, MSI, NSIS, NSIS Web, Portable, AppX/MSIX, DEB, RPM, AppImage, RawAppImage, Pacman, Snap, Flatpak, ZIP, TAR, and 7Z, with signing and notarization. - **CI support** — reusable GitHub Actions, multi-platform matrix builds, universal macOS binaries, and MSIX bundles. ## What's next - [Nucleus vs vanilla Compose Multiplatform](/docs/compare/cmp-default) — the decision table for Kotlin teams. - [Migrating from JetBrains Compose Desktop](/docs/migrate/from-jb-compose) — drop-in steps. - [Quickstart](/docs/start/quickstart) — build and run a Tao window. - [Architecture](/docs/concepts/architecture) — layers from your app down to the JVM or GraalVM. --- # Install URL: https://nucleusframework.dev/en/docs/start/install Add the Nucleus Gradle plugin to a Kotlin project to build for macOS, Linux, and Windows from a single build. In this tutorial, you'll add the Nucleus Gradle plugin to a Kotlin project. Once the plugin is applied, the same build can compile, run, and package a desktop app for macOS, Linux, and Windows. ## Before you start Nucleus builds on Compose Multiplatform and requires: - **JDK 17 or later** on your `PATH`. - **Kotlin 2.4.10 or later** — see [Compatibility](#compatibility) for the version each Nucleus line needs. - **Gradle 8.0 or later**. The Gradle wrapper bundled with new projects is sufficient. JDK 25 or later is required only to generate the AOT cache (`enableAotCache` in `nativeDistributions`). Any JDK 17+ works for development. ## Compatibility | Nucleus Version | Kotlin Version | Compose Version | |-----------------|----------------|-----------------| | 2.5.0 – 2.5.18 | 2.4.10+ | 1.12.0 | | 2.0.0 – 2.4.7 | 2.4.0+ | 1.11.1 | The Compose version is a requirement, not a floor: `decorated-window-tao` builds against Compose Multiplatform internals that change between minors, so 2.5.x needs 1.12.0 and will not run on 1.11.x. Pin the Compose plugin to the row that matches your Nucleus version. The Kotlin version is a minimum. Nucleus is compiled with the version in the table, and an older compiler cannot read its metadata. ## Apply the plugin In your module's `build.gradle.kts`, apply the Nucleus plugin alongside the Kotlin and Compose plugins: ```kotlin title="build.gradle.kts" plugins { kotlin("jvm") version "2.4.10" id("org.jetbrains.compose") version "1.12.0" id("dev.nucleusframework") version "" } ``` The `plugins` block resolves `dev.nucleusframework` from the Gradle Plugin Portal, so no extra `pluginManagement` repository is required. ## Add the repositories Nucleus runtime modules publish to Maven Central under the group `dev.nucleusframework`. Add the repositories the plugin and its runtime need: ```kotlin title="build.gradle.kts" repositories { mavenCentral() google() } ``` ## Verify the plugin is applied Applying the plugin registers a `nucleus` task group. List its tasks to confirm the plugin loaded: ```shell ./gradlew tasks --group nucleus ``` If the command prints the group's tasks, the plugin is applied and you can start building. ## What's next - [Quickstart](/docs/start/quickstart) — write a `main` function, run a native Tao window, and package an installer. - [Project setup](/docs/start/project-setup) — repositories, Gradle properties, JDK toolchains, and multi-module layout. - [Configuration](/docs/start/configuration) — the full `nucleus { }` DSL. --- # Quickstart URL: https://nucleusframework.dev/en/docs/start/quickstart Build and run your first Nucleus desktop app, then package it as a native installer. In this tutorial, you'll create a Nucleus desktop app from scratch, run it as a native Tao window, and package it into an installer for your current operating system. The whole app is two files: a Gradle build script and a Kotlin `main` function. ## Before you start Nucleus builds on Compose Multiplatform and requires: - **JDK 17 or later** on your `PATH`. - **Kotlin 2.4.10 or later** and **Gradle 8.x**. ## Create the build script In an empty project directory, create `build.gradle.kts` and apply the Nucleus plugin: ```kotlin title="build.gradle.kts" plugins { kotlin("jvm") version "2.4.10" id("org.jetbrains.compose") version "1.12.0" id("dev.nucleusframework") version "" } repositories { mavenCentral() google() } dependencies { implementation(compose.desktop.currentOs) // The entry-point module — provides nucleusApplication and DecoratedWindow implementation("dev.nucleusframework:nucleus.nucleus-application:") // Enable the Tao backend (Rust-native windowing) implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } nucleus.application { mainClass = "com.example.MainKt" nativeDistributions { packageName = "MyApp" packageVersion = "1.0.0" targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) } } ``` Already have a Compose Desktop app? Nucleus extends `org.jetbrains.compose`, so switching is a rename — drop the vanilla `compose.desktop.application { }` block and point it at Nucleus: ```diff -compose.desktop.application { +nucleus.application { mainClass = "com.example.MainKt" // nativeDistributions { ... } stays the same } ``` The entry point in `main` follows the same swap: `application { Window(...) }` becomes `nucleusApplication { DecoratedWindow(...) }`. See [Migrating from Compose Desktop](/docs/migrate/from-jb-compose). ## Write the application Create `src/main/kotlin/com/example/Main.kt`: ```kotlin package com.example import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text import androidx.compose.ui.Modifier import dev.nucleusframework.application.DecoratedWindow import dev.nucleusframework.application.NucleusBackend import dev.nucleusframework.application.nucleusApplication fun main() = nucleusApplication(backend = NucleusBackend.Tao) { DecoratedWindow( onCloseRequest = ::exitApplication, title = "MyApp", ) { Box(Modifier.fillMaxSize()) { Text("Hello from Nucleus") } } } ``` The `nucleusApplication` entry point starts the Nucleus runtime and opens a `DecoratedWindow` — a Compose window with a native title bar. `NucleusBackend.Auto` (the default) picks Tao when `decorated-window-tao` is on the classpath, and falls back to the deprecated AWT backend otherwise. Force a backend with `NucleusBackend.Tao`. See [Backends](/docs/concepts/backends). ## Run the app ```shell ./gradlew run ``` A native window opens with a custom title bar — drawn by Tao on Linux and Windows, and by the system on macOS. ## Package the app ```shell ./gradlew packageDistributionForCurrentOS ``` The installer is written to `build/compose/binaries/main//`: a `.dmg` on macOS, an `.msi` on Windows, and a `.deb` on Linux. Add formats by extending `targetFormats(...)`. See [Configuration](/docs/start/configuration). ## What's next - [Project setup](/docs/start/project-setup) — multi-module layout, Gradle properties, and JDK config. - [Configuration](/docs/start/configuration) — the full `nucleus { }` DSL. - [Architecture](/docs/concepts/architecture) — what runs under your `main` function. --- # Project setup URL: https://nucleusframework.dev/en/docs/start/project-setup Configure repositories, the Nucleus plugin, runtime modules, a JDK toolchain, and a multi-module Gradle layout. In this tutorial, you'll set up a multi-module Nucleus project. You'll configure the repositories, apply the Gradle plugin, pin the runtime modules, choose a JDK toolchain, and lay out `:app`, `:ui`, and `:domain` modules so that dependencies flow in one direction. ## Before you start This tutorial builds on the [Quickstart](/docs/start/quickstart), which fits in a single module. You need: - **JDK 17 or later** for a standard build, or **JDK 25** to enable the AOT cache. - **Kotlin 2.4.10 or later** and **Gradle 8.x**. ## Configure the repositories Resolve the plugin from the Gradle Plugin Portal and the runtime modules from Maven Central. Add both to `settings.gradle.kts`: ```kotlin title="settings.gradle.kts" pluginManagement { repositories { gradlePluginPortal() mavenCentral() google() } } dependencyResolutionManagement { repositories { mavenCentral() google() } } ``` ## Apply the plugin Apply the Nucleus plugin in the module that holds your `main` function: ```kotlin title="app/build.gradle.kts" plugins { kotlin("jvm") version "2.4.10" id("org.jetbrains.compose") version "1.12.0" id("dev.nucleusframework") version "" } ``` Compose Hot Reload works out of the box. Apply the `org.jetbrains.compose.hot-reload` plugin, then run `./gradlew hotRun` — Nucleus resolves the entry point from `nucleus.application.mainClass` automatically, with no `ComposeHotRun` configuration. `hotDev` without `-PclassName` behaves like `hotRun`. Hot reload was added in Nucleus 2.1. ## Add the runtime modules All `dev.nucleusframework:nucleus.*` runtime artifacts publish the same version. Pin it once, then declare only the modules you use: ```kotlin title="app/build.gradle.kts" val nucleusVersion = "" dependencies { implementation(compose.desktop.currentOs) implementation("dev.nucleusframework:nucleus.nucleus-application:$nucleusVersion") implementation("dev.nucleusframework:nucleus.decorated-window-tao:$nucleusVersion") // Add only the modules you need: implementation("dev.nucleusframework:nucleus.notification-common:$nucleusVersion") implementation("dev.nucleusframework:nucleus.global-hotkey:$nucleusVersion") } ``` `nucleus-application` is the entry-point module. It transitively pulls the core runtime, the AOT runtime, and the GraalVM runtime. Add `decorated-window-tao` for the window backend; every other module is opt-in. ## Set the JDK toolchain Pin a JDK toolchain so the build uses a consistent Java version: ```kotlin title="app/build.gradle.kts" kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(25)) // Optional: pin the vendor for reproducible builds // vendor.set(JvmVendorSpec.JETBRAINS) } } ``` Use JDK 17 if you don't need the AOT cache. Use JDK 25 to enable it with `enableAotCache = true` inside `nativeDistributions`. For GraalVM native images, set the Java version with `nucleus.application.graalvm { javaLanguageVersion.set(25) }`. See [Configuration](/docs/start/configuration) for both. ## Configure gradle.properties These settings speed up the build and enable the configuration cache, which the Nucleus plugin supports: ```properties title="gradle.properties" org.gradle.jvmargs=-Xmx4g -XX:+UseG1GC org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true kotlin.code.style=official ``` ## Lay out the modules A single module works for small apps. As the app grows, split UI, application wiring, and business logic into separate modules: ``` my-app/ ├── settings.gradle.kts ├── build.gradle.kts ← shared conventions, no plugins ├── app/ ← Nucleus entry point + main() │ └── build.gradle.kts ← applies dev.nucleusframework ├── ui/ ← Compose composables, themes │ └── build.gradle.kts └── domain/ ← pure Kotlin: models, repositories, services └── build.gradle.kts ``` Follow these rules to keep dependencies flowing in one direction: - Only `:app` applies `dev.nucleusframework`, and only `:app` declares `nucleus.application { … }`. - `:domain` stays pure Kotlin, so you can share it with Android or iOS later by moving it into a Kotlin Multiplatform `commonMain` source set. - Platform-specific code, such as a macOS dock menu or a Windows jump list, lives next to the code that uses it, most often inside `:app`. ## Share code across platforms The Nucleus runtime modules are JVM-only, but `:domain` and `:ui` can be Kotlin Multiplatform modules. The `:app` module consumes their JVM target and needs no change when you add other targets. ## What's next - [Quickstart](/docs/start/quickstart) — build and run a single-module app first. - [Configuration](/docs/start/configuration) — the full `nucleus { }` Gradle DSL. - [Modules](/docs/concepts/modules) — how Nucleus modules compose. - [Runtimes](/docs/concepts/runtimes) — the core, AOT, and GraalVM runtimes. --- # Configuration URL: https://nucleusframework.dev/en/docs/start/configuration Configure the nucleus { } Gradle DSL — application metadata, packaging formats, per-OS options, GraalVM native image, AOT cache, deep links, file associations, and trusted CA injection. The `nucleus { }` extension configures how your app is built and packaged, in your `build.gradle.kts`. It extends the Compose Desktop `application` DSL with a GraalVM native-image path, additional packaging formats, an AOT cache, deep-link protocols, and trusted CA injection. ## Configure the application Use `nucleus.application { }` for a JVM application. The `nativeDistributions { }` block holds all packaging configuration, and the `graalvm { }` block controls the native-image path. ```kotlin title="build.gradle.kts" nucleus.application { mainClass = "com.example.MainKt" jvmArgs("-Xmx512m") nativeDistributions { packageName = "MyApp" packageVersion = "1.0.0" targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) modules("java.sql", "java.net.http") cleanupNativeLibs = true protocol("MyApp", "myapp") fileAssociation( mimeType = "application/x-myapp", extension = "myapp", description = "MyApp Document", ) macOS { /* … */ } windows { /* … */ } linux { /* … */ } } graalvm { isEnabled.set(false) } } ``` To configure a Kotlin/Native application instead, use `nucleus.nativeApplication { }`. ## Choose a runtime strategy Nucleus can ship your app as a bundled JVM, a GraalVM native image, or a JVM with an AOT cache. **Bundled JVM** ```kotlin title="build.gradle.kts" nucleus.application { mainClass = "com.example.MainKt" nativeDistributions { packageName = "MyApp" packageVersion = "1.0.0" targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) cleanupNativeLibs = true } } ``` Bundles a JRE alongside your app. This is the default and requires the least configuration. **GraalVM native image** ```kotlin title="build.gradle.kts" nucleus.application { mainClass = "com.example.MainKt" nativeDistributions { packageName = "MyApp" packageVersion = "1.0.0" targetFormats(TargetFormat.Dmg, TargetFormat.Nsis, TargetFormat.Deb) } graalvm { isEnabled.set(true) javaLanguageVersion.set(25) imageName.set("myapp") march.set(NativeImageMarch.NATIVE) buildArgs.add("-O2") } } ``` Compiles the app to a self-contained native binary. Reachability metadata for Nucleus modules is injected automatically. **JVM with AOT cache** ```kotlin title="build.gradle.kts" nucleus.application { mainClass = "com.example.MainKt" nativeDistributions { packageName = "MyApp" packageVersion = "1.0.0" targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) enableAotCache = true } } ``` Requires JDK 25 or later. The build generates an AOT cache and ships it with the launcher, so classes and profiles are pre-warmed at startup. ## `application { }` reference | Property | Type | Description | |---|---|---| | `mainClass` | `String?` | Entry-point class (FQCN of the file containing `fun main`). | | `mainJar` | `RegularFileProperty` | Override the main JAR; rarely needed. | | `javaHome` | `String` | JDK used for packaging the JVM image. | | `args` | `MutableList` | App arguments at launch. Also `args(vararg String)`. | | `jvmArgs` | `MutableList` | JVM flags baked into the launcher. Also `jvmArgs(vararg String)`. | | `nativeDistributions { }` | block | Packaging configuration. | | `buildTypes { release { proguard { } } }` | block | ProGuard rules for release builds. | | `graalvm { }` | block | GraalVM native-image configuration. | | `disableDefaultConfiguration()` | fn | Opt out of the auto-discovered source set. | | `from(SourceSet \| KotlinTarget)` | fn | Pick a source set or KMP target explicitly. | ## `nativeDistributions { }` reference | Property / call | Description | |---|---| | `targetFormats(...)` | Select packaging formats. See [the format list](#select-packaging-formats) below. | | `packageName`, `packageVersion`, `description`, `vendor`, `copyright`, `homepage`, `licenseFile` | App metadata. `homepage` is required for Linux DEB. | | `modules(...)`, `includeAllModules` | JDK modules to include. Default: `java.base`, `java.desktop`, `java.logging`, `java.net.http`, `jdk.accessibility`, `jdk.crypto.ec`, `jdk.unsupported`. | | `cleanupNativeLibs` | Strip non-target native libraries from dependency JARs. | | `enableAotCache` | Generate a JDK 25+ AOT cache. | | `splashImage` | Path (relative to app resources) to a splash PNG. | | `compressionLevel` | `CompressionLevel.Store`, `Normal`, `Maximum`, or `Ultra` (extra DMG/DEB recompression). Overridable per format via `linux.appImage { compressionLevel }` and `windows.portable { compressionLevel }`. | | `artifactName` | Template. Default: `${name}-${version}-${os}-${arch}.${ext}`. | | `trustedCertificates.from(...)` | Extra CA bundles imported into the bundled JDK `cacerts`. | | `protocol(name, vararg schemes)` | Register a custom URL protocol (deep link). | | `fileAssociation(mimeType, extension, description, linuxIconFile?, windowsIconFile?, macOSIconFile?)` | Register a file type. Call multiple times for multiple associations. | | `publish { }` | GitHub Releases, S3, or generic HTTP publishing config. | | `macOS { }`, `windows { }`, `linux { }` | Per-OS overrides. | ## Select packaging formats `targetFormats(...)` takes any of the 18 `TargetFormat` values: | OS | Formats | |---|---| | macOS | `Dmg`, `Pkg` | | Windows | `Msi`, `Exe`, `Nsis`, `NsisWeb`, `Portable`, `AppX` | | Linux | `Deb`, `Rpm`, `AppImage`, `Pacman`, `Snap`, `Flatpak` | | Any | `Zip`, `Tar`, `SevenZ`, `RawAppImage` | ```kotlin title="build.gradle.kts" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) } ``` `Pkg`, `AppX`, and `Flatpak` are the store formats. See [Packaging](/docs/packaging) for per-format details. ## Set per-OS options Each `macOS`, `windows`, and `linux` block overrides metadata and behavior for that target. ```kotlin title="build.gradle.kts" nativeDistributions { macOS { bundleID = "com.example.myapp" dockName = "MyApp" appCategory = "public.app-category.productivity" minimumSystemVersion = "12.0" iconFile.set(project.file("icons/app.icns")) infoPlist { extraKeysRawXml = """ NSCameraUsageDescription MyApp needs the camera for QR scanning. """.trimIndent() } signing { sign.set(true) identity.set("Developer ID Application: My Company (TEAMID)") } notarization { appleID.set(System.getenv("APPLE_ID")) password.set(System.getenv("APPLE_APP_PASSWORD")) teamID.set("TEAMID") } // macOS SDK version linked into the launcher macOsSdkVersion = "26.0" } windows { packageName = "MyApp" upgradeUuid = "00000000-0000-0000-0000-000000000000" menu = true perUserInstall = true iconFile.set(project.file("icons/app.ico")) signing { // Authenticode signing config } } linux { packageName = "myapp" appCategory = "Utility" menuGroup = "Utility" debMaintainer = "ops@example.com" iconFile.set(project.file("icons/app.png")) rpmLicenseType = "MIT" } } ``` See [Code signing](/docs/packaging/code-signing) for the full signing and notarization surface. ## Register deep links ```kotlin title="build.gradle.kts" nativeDistributions { protocol("MyApp", "myapp") } ``` This wires `CFBundleURLTypes` on macOS, registry entries on Windows, and `MimeType=x-scheme-handler/myapp` in the Linux `.desktop` file. Receive the URI at runtime through `NucleusApplicationScope.onDeepLink { uri -> … }`. See [Deep links](/docs/lifecycle/deep-links). ## Associate file types ```kotlin title="build.gradle.kts" nativeDistributions { fileAssociation( mimeType = "application/x-myapp", extension = "myapp", description = "MyApp Document", linuxIconFile = project.file("icons/file.png"), windowsIconFile = project.file("icons/file.ico"), macOSIconFile = project.file("icons/file.icns"), ) } ``` Call `fileAssociation(...)` once per file type. ## Inject trusted CA certificates ```kotlin title="build.gradle.kts" nativeDistributions { trustedCertificates.from(files("certs/company-proxy-ca.pem")) } ``` Accepts PEM or DER. The certificates are imported into the bundled JDK's `cacerts` at build time, which is useful behind a corporate proxy. To trust the OS keychain at runtime instead, use [`native-ssl`](/docs/performance/native-ssl). ## Configure a GraalVM native image | Property | Default | Description | |---|---|---| | `isEnabled` | `false` | Master switch. | | `javaLanguageVersion` | `25` | GraalVM JDK to use. | | `jvmVendor` | (auto) | Override the toolchain vendor. | | `imageName` | (auto) | Output binary name. | | `march` | `NativeImageMarch.COMPATIBILITY` (`NATIVE` on macOS arm64) | Target CPU instruction set. | | `buildArgs` | `[]` | Extra `native-image` flags. | | `nativeImageConfigBaseDir` | (build dir) | Where to write reflection and resource configs. | | `metadataRepository.enabled` | `true` | Use the GraalVM Reachability Metadata Repository. | | `metadataRepository.version` | `"1.1.4"` | Repository version. | | `metadataRepository.excludedModules` | `[]` | Skip metadata for specific modules. | | `macOS.minimumSystemVersion` | `"12.0"` | Minimum macOS target. | | `macOS.macOsSdkVersion` | `"26.0"` | macOS SDK linked into the launcher. | | `windows.bundleCRuntime` | `true` | Bundle the MSVC runtime DLLs next to the binary. | See [GraalVM](/docs/performance/graalvm) for the full walkthrough. ## Enable the AOT cache ```kotlin title="build.gradle.kts" nativeDistributions { enableAotCache = true } ``` Requires JDK 25 or later. The plugin runs a training pass that boots your app to the first frame, snapshots the JVM's class state, and ships the cache alongside the launcher. See [AOT cache](/docs/performance/aot-cache). ## Shrink release builds with ProGuard ```kotlin title="build.gradle.kts" buildTypes { release { proguard { isEnabled = true optimize = true obfuscate = false joinOutputJars = true configurationFiles.from(project.file("proguard-rules.pro")) } } } ``` Release tasks are suffixed `Release`, for example `packageReleaseDmg`. `CompressionLevel.Maximum` and `Ultra` with AppImage cause very slow startup due to squashfs/FUSE decompression. Use `Normal` or `Store` for AppImage targets — set `linux.appImage { compressionLevel = CompressionLevel.Normal }` when the root level is higher for DEB/DMG. ## What's next - [Packaging](/docs/packaging) — per-format details, code signing, and store publishing. - [GraalVM](/docs/performance/graalvm) — reachability metadata, build args, and CI. - [Gradle DSL reference](/docs/reference/gradle-dsl) — every property and type. --- # Architecture URL: https://nucleusframework.dev/en/docs/concepts/architecture The four layers that make up a Nucleus application and how they fit together. A Nucleus application is built from four layers: your code, the Nucleus runtime, Compose Multiplatform, and the JVM or native runtime underneath. Each layer has a distinct job, and each one can be swapped without rewriting the layer above it. This page describes what each layer does and how a frame reaches the screen. ## The four layers ### Your application Compose UI, view-models, and business logic, written in Kotlin. To share this code with Android and iOS, place it in a Kotlin Multiplatform `commonMain` source set. ```kotlin import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue // Pure Kotlin — runs on every platform Compose targets. class CounterViewModel { var count by mutableStateOf(0) private set fun increment() { count++ } } ``` ### The Nucleus runtime More than 40 runtime modules, grouped by intent: - **OS integration** — decorated windows (Tao backend), notifications, system tray, dock and launcher menus, dark-mode detection, global hotkeys, media controls, and system colors. - **Performance** — GraalVM Native Image, the AOT cache (JDK 25+), the energy manager, and native HTTP/SSL. - **Distribution** — 18 packaging formats, code signing and notarization, auto-update, deep links, auto-launch, and reusable CI actions. Every module is optional and written in Kotlin. You add the ones you need as Gradle dependencies — see [Modules](/docs/concepts/modules). The Gradle plugin (`dev.nucleusframework`) is the build-time half of this layer. It selects packaging formats, wires up GraalVM, injects reachability metadata, and generates `nucleus-app.properties` so that `NucleusApp` in `core-runtime` can read your application's identity at runtime. ### Compose Multiplatform A type-safe, reactive UI toolkit rendered on Skia, with hot reload, Compose Resources, and a unified pointer and keyboard event model. Nucleus does not replace Compose; it plugs into it. Every `DecoratedWindow` is a Compose `@Composable`, and every Nucleus module that touches the UI — `isSystemInDarkMode()`, `systemAccentColor()`, `NativeMenuBar`, and others — is a composable too. ### Kotlin Multiplatform Shared modules, coroutines, `StateFlow`, `kotlinx.serialization`, and Ktor: the same Kotlin Multiplatform stack you already use for Android and iOS. The Nucleus runtime modules are JVM-only, but the layers above them stay multiplatform. ### The JVM or native runtime A Nucleus application runs on one of three runtimes: - **OpenJDK 17+** — the broadest compatibility, works with the Tao backend on stock JDKs. - **JetBrains Runtime (JBR)** — an OpenJDK fork required only by the deprecated AWT backend (see [Backends](/docs/concepts/backends)). - **GraalVM Native Image** — a closed-world, ahead-of-time compile that produces a self-contained binary with no bundled JRE. Switching runtimes is a one-line DSL change — see [Runtimes](/docs/concepts/runtimes). A Nucleus application requires JDK 17 or later. Most runtime modules compile to Java 11 bytecode, so code that embeds them stays compatible with older toolchains. The Compose-facing ones — `nucleus-application`, the window backends and the style adapters — target Java 17, and `decorated-window-jewel` targets Java 25. ## How a frame is drawn When the user clicks a button: 1. The OS — or the [Tao](/docs/tao) Rust layer — emits an input event. 2. The active backend module ([`decorated-window-tao`](/docs/concepts/backends)) translates it into a Compose `PointerEvent`. 3. Compose recomposes, asks Skia to draw the affected layers, and hands the GPU surface back to the backend. 4. The backend swaps buffers through the OS compositor (Wayland, DWM, or Quartz). Your code stays in Kotlin the whole way down. ## What's next - [Runtimes](/docs/concepts/runtimes) — choosing GraalVM Native Image versus the JVM with the AOT cache. - [Backends](/docs/concepts/backends) — Tao, and the deprecated AWT backend, under your `DecoratedWindow`. - [Modules](/docs/concepts/modules) — how the runtime modules compose. --- # Runtimes URL: https://nucleusframework.dev/en/docs/concepts/runtimes Nucleus can run your packaged app as a GraalVM native image or on the JVM with an AOT cache, selected from the Gradle build. A Nucleus app can run on one of two runtimes: a GraalVM native image, or the JVM with an ahead-of-time (AOT) cache. Both build from the same code — you choose the runtime in your Gradle build. This page describes each runtime, the tradeoffs between them, and how to detect which one your app is running under. ## GraalVM native image A native image compiles your app ahead of time into a single self-contained binary. It bundles no JRE, so the installer only ships one executable per operating system. Enable it in the plugin DSL: ```kotlin title="build.gradle.kts" nucleus.application { graalvm { isEnabled.set(true) } } ``` A native image is closed-world: reflection, dynamic class loading, and JNI must be known at build time. Nucleus resolves reachability metadata automatically — for your third-party libraries too, not just its own modules — from a curated set, the Oracle repository, and static bytecode analysis, so most apps compile without hand-written config. See [automatic metadata resolution](/docs/performance/graalvm/automatic-metadata). Because there is no JVM warmup, a native image reaches its first frame quickly. The tradeoff is the closed-world constraint: no runtime class loading, no Java agents, no JFR, and no dynamic `ScriptEngine`. If your app loads plugins or generates classes at runtime, use the JVM runtime instead. ## JVM with an AOT cache The JVM runtime ships the full HotSpot runtime and keeps every dynamic feature: reflection, dynamic class loading, agents, JFR, and `ScriptEngine`. The AOT cache pre-warms classes and profiles so the app starts faster than a cold JVM while HotSpot's JIT still compiles hot paths. Enable the cache under `nativeDistributions`: ```kotlin title="build.gradle.kts" nucleus.application { nativeDistributions { enableAotCache = true } } ``` The cache adds a few megabytes to the distribution. See [AOT cache](/docs/performance/aot-cache) for how training runs produce the cache and what it contains. ## Choosing a runtime The two runtimes trade startup and binary size against dynamic capability: | | GraalVM native image | JVM + AOT cache | |---|---|---| | Compilation | Ahead-of-time, closed-world | HotSpot JIT, AOT cache warms startup | | Bundled runtime | None (self-contained binary) | Full HotSpot JRE | | Dynamic features | Restricted; must be declared as reachability metadata | Available | | Startup | No JVM warmup | Slower cold, improved by the AOT cache | | Throughput | AOT-compiled | JIT reaches higher throughput on hot paths | Use a native image for a fixed app surface where startup and installer size matter most. Use the JVM runtime for plugin hosts, developer tools, or anything that relies on runtime dynamism. ## Detecting the runtime at runtime The [`aot-runtime`](/docs/performance/aot-cache) module reports the current AOT mode, and [`core-runtime`](/docs/lifecycle/executable-type) reports the package format the process was launched from. ```kotlin import dev.nucleusframework.aot.runtime.AotRuntime import dev.nucleusframework.aot.runtime.AotRuntimeMode import dev.nucleusframework.core.runtime.ExecutableRuntime import dev.nucleusframework.core.runtime.ExecutableType when (AotRuntime.mode()) { AotRuntimeMode.TRAINING -> { /* AOT training pass — skip user-facing work */ } AotRuntimeMode.RUNTIME -> { /* launched with a warmed AOT cache */ } AotRuntimeMode.OFF -> { /* plain JVM, or a GraalVM native image */ } } // Which package format is this process running from? val type: ExecutableType = ExecutableRuntime.type() // EXE, MSI, NSIS, NSIS_WEB, PORTABLE, APPX, DMG, PKG, DEB, RPM, // SNAP, FLATPAK, APPIMAGE, PACMAN, ZIP, TAR, SEVEN_Z, DEV // Running inside a GraalVM native image? val isNative: Boolean = ExecutableRuntime.isGraalVmNativeImage ``` `AotRuntime.mode()` returns `OFF` on a plain JVM and inside a native image, since the AOT cache is a JVM-only mechanism. `AotRuntime.isRuntime()` and `AotRuntime.isTraining()` are shorthands for the `RUNTIME` and `TRAINING` modes. ## What's next - [GraalVM](/docs/performance/graalvm) — reachability metadata, build arguments, and CI integration. - [AOT cache](/docs/performance/aot-cache) — training runs, lifecycle, and what gets cached. - [Executable type](/docs/lifecycle/executable-type) — the full `ExecutableType` and `ExecutableRuntime` API. - [Configuration](/docs/start/configuration) — the full Gradle DSL. --- # Backends URL: https://nucleusframework.dev/en/docs/concepts/backends Nucleus draws every window through the Tao backend behind the DecoratedWindow API. The legacy AWT backend is deprecated. Every Nucleus window is a Compose composable. The backend is the layer beneath it: it runs the OS event loop, owns the native window handle, and presents Skia's GPU surface on screen. Nucleus 2.0 uses Tao as its backend behind the same `DecoratedWindow` API. The legacy AWT backend (`decorated-window-jbr` / `decorated-window-jni`) is deprecated and will be removed in a future Nucleus release. New and existing projects should move to Tao — see [Migrating from JBR to Tao](/docs/tao/migration-from-jbr). ## The Tao backend Tao is the Rust windowing layer that also underpins Tauri 2. Nucleus makes it the default for new projects. Tao replaces the toolkit entirely, so the JVM never loads AWT. The Tao backend adds: - **Native Wayland.** No XWayland fallback, with fractional scaling and gestures. - **Multi-touch and gestures.** Pinch, swipe, and rotate arrive as Compose pointer events that carry pressure, tilt, and source. - **Pen and stylus.** Pressure-sensitive input from devices such as Wacom tablets, the Surface Pen, and Apple Pencil. - **Per-monitor HiDPI.** Mixed-DPI setups are handled as you drag a window between displays. - **`NativeView`.** Embed any platform-native view inside a Compose window — `NSView` on macOS, `HWND` on Windows, `GtkWidget` on Linux (SwiftUI, Win32/WinUI, WebView2, GTK, and anything else that exposes those handles). See [Tao](/docs/tao) for the full surface. ## Select a backend ```kotlin import dev.nucleusframework.application.NucleusBackend import dev.nucleusframework.application.nucleusApplication fun main() = nucleusApplication(backend = NucleusBackend.Tao) { DecoratedWindow(onCloseRequest = ::exitApplication, title = "MyApp") { // your Compose UI } } ``` `NucleusBackend` has three values: - `Auto` uses Tao when `decorated-window-tao` is on the classpath, otherwise the deprecated AWT backend. This is the default. - `Tao` forces the Tao backend. - `Awt` forces the deprecated AWT backend, using JBR or JNI depending on which is resolved. ## Reach the native window The `DecoratedWindow` composable is identical across backends. When you need a backend-specific call, reach the underlying handle through the window's escape hatches: - `nucleusWindow.unsafe.taoWindow` returns the Tao-owned `TaoWindow`. - `nucleusWindow.unsafe.awtWindow` returns the AWT `ComposeWindow` (deprecated backend only). Each returns `null` on the other backend. When `decorated-window-tao` is on the classpath, the Gradle plugin injects `-XstartOnFirstThread` into Hot Reload's `JavaExec` tasks on macOS, because Tao runs the OS event loop on the process main thread. ## What's next - [Tao](/docs/tao) — the full backend surface: gestures, pen, Wayland, and `NativeView`. - [Decorated Window](/docs/window) — the window you compose on top of a backend. - [Migrating from JBR to Tao](/docs/tao/migration-from-jbr) — a checklist for existing apps. --- # Modules URL: https://nucleusframework.dev/en/docs/concepts/modules How Nucleus splits its runtime into independently versioned Gradle modules and how to add the ones you need. Nucleus is a set of independently versioned Gradle modules. One umbrella module brings the runtime essentials, and every other capability is a separate module you add when you need it, so your build carries only what your app uses. ## The umbrella module Add `nucleus-application` to pull in the runtime essentials: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") } ``` This pulls in the runtime modules every app needs: - `core-runtime` — platform detection, [native-library loading](/docs/performance/native-library-loader), the deep-link handler, the single-instance lock, and app metadata. - `aot-runtime` — AOT cache mode detection. - `graalvm-runtime` — the SVM substitutions and the initializer called from `nucleusApplication { }`. - `darkmode-detector` — reactive OS theme, exposed as `api` so `isSystemInDarkMode()` needs no extra dependency. - `spellcheck` — the [native spell engines](/docs/os/spell-check), also exposed as `api`. It also brings the shared Compose window code in `decorated-window-core`, so you add only a window backend on top. `core-runtime` and `graalvm-runtime` are `implementation` dependencies — present at run time, not on your compile classpath. Everything else is opt-in. ## Window modules Every app uses exactly one window backend, plus an optional style adapter. | Module | Role | |---|---| | `decorated-window-core` | Shared Compose styling, themes, and icon sets. Pulled in by the others. | | `decorated-window-tao` | Rust-native backend ([Tao](/docs/concepts/backends)). Default for new projects. | | `decorated-window-jbr` / `decorated-window-jni` / `decorated-window-awt` | AWT backend. **Deprecated**, will be removed in a future Nucleus release. | | `decorated-window-jewel` | Style adapter — IntelliJ Platform look. | | `decorated-window-material2` | Style adapter — Material 2. | | `decorated-window-material3` | Style adapter — Material 3. | Full design systems (widgets + theme + optional window) live in **separate repositories**, versioned independently: | Design system | Window artifact | Repo | |---------------|-----------------|------| | macOS 26 | `dev.nucleusframework:compose-macos-ui` | [compose-macos-26-ui](https://github.com/NucleusFramework/compose-macos-26-ui) | | Fluent | `dev.nucleusframework.composefluent:decorated-window-fluent` | [compose-fluent-ui](https://github.com/NucleusFramework/compose-fluent-ui) | | Yaru | `dev.nucleusframework.yarucompose:yaru-decorated-window` | [yaru-compose-ui](https://github.com/NucleusFramework/yaru-compose-ui) | See [Native design systems](/docs/window/toolkits). Pick exactly one backend (`-tao`, or one of the deprecated AWT modules). Style adapters and external design systems are optional. ## OS integration modules | Capability | Module | |---|---| | Notifications (cross-platform DSL) | `notification-common` | | Notifications (macOS) | `notification-macos` | | Notifications (Windows Toast) | `notification-windows` | | Notifications (Linux DBus) | `notification-linux` | | Dock menu (macOS) | `launcher-macos` | | Jump lists, taskbar buttons (Windows) | `launcher-windows` | | Unity quicklist (Linux) | `launcher-linux` | | Native menu bar (macOS) | `menu-macos` | | Taskbar progress | `taskbar-progress`, [`taskbar-progress-tao`](/docs/tao/taskbar-progress) | | Dark-mode detection | `darkmode-detector` | | System accent color | `system-color` | | Global hotkeys | `global-hotkey` | | Media controls (Now Playing / SMTC / MPRIS) | `media-control` | | System info (CPU, RAM, GPU, batteries) | `system-info` | | Energy / efficiency mode | `energy-manager` | | Auto-launch at login | `autolaunch` | | Scheduler (cron / periodic / boot) | `scheduler` | | Scheduler testing (virtual clock) | [`scheduler-testing`](/docs/lifecycle/scheduler-testing) | | File-system watcher | [`fs-watcher`](/docs/lifecycle/fs-watcher) | | macOS service management | `service-management-macos` | | Linux HiDPI scale detection | `linux-hidpi` | | SF Symbols catalogue | [`sf-symbols`](/docs/os/sf-symbols) | | FreeDesktop icon names | `freedesktop-icons` | ## Performance and networking modules | Capability | Module | |---|---| | Native trust manager (OS keychain CAs) | `native-ssl` | | `java.net.http.HttpClient` with native trust | `native-http` | | OkHttp with native trust | [`native-http-okhttp`](/docs/performance/native-http) | | Ktor with native trust | [`native-http-ktor`](/docs/performance/native-http) | | Auto-update | `updater-runtime` | ## Add a module Declare the umbrella, one window backend, and any capability modules you need: ```kotlin title="build.gradle.kts" val nucleusVersion = "" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:$nucleusVersion") implementation("dev.nucleusframework:nucleus.decorated-window-tao:$nucleusVersion") // Add the capabilities you need implementation("dev.nucleusframework:nucleus.notification-common:$nucleusVersion") implementation("dev.nucleusframework:nucleus.global-hotkey:$nucleusVersion") implementation("dev.nucleusframework:nucleus.system-info:$nucleusVersion") } ``` Pin every `nucleus.*` artifact to the same version. The modules release together. ## How modules compose Most modules depend on `core-runtime` for platform detection and app metadata. A few standalone catalogues, such as `sf-symbols` and `freedesktop-icons`, carry no runtime dependency. Some modules depend on each other: `notification-common` orchestrates the per-OS notification modules, and `taskbar-progress-tao` depends on `nucleus-application` to add extension functions on `NucleusWindow`. The Gradle plugin injects `nucleus-app.properties` into the classpath at build time, so any module reads your app's identity through `NucleusApp.appId`, `NucleusApp.version`, `NucleusApp.aumid`, and the other properties without extra wiring. When the GraalVM build runs, each module's reachability metadata is bundled in automatically, so there is no reflection config to maintain by hand. ## What's next - [Architecture](/docs/concepts/architecture) — how the layers fit together. - [OS integration](/docs/os) — concrete recipes per capability. - [Reference: Gradle DSL](/docs/reference/gradle-dsl) — full coordinates and version table. --- # The Tao backend URL: https://nucleusframework.dev/en/docs/tao Tao is a Rust-native window backend that runs Compose Desktop without AWT and adds native Wayland, multi-touch, and pen input. Tao is the Rust windowing crate that also underpins Tauri 2. Nucleus wraps it as a window backend: Compose Desktop renders into a native OS window, so there is no Swing event-dispatch thread and no XWayland fallback. Tao is the backend for new Nucleus apps. The legacy AWT backend is legacy and maintenance-only — see [migration from JBR](/docs/tao/migration-from-jbr). ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-core:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` The Nucleus Gradle plugin detects `decorated-window-tao` on the classpath and selects the Tao backend at runtime. To select it explicitly, pass `NucleusBackend.Tao`: ```kotlin nucleusApplication(backend = NucleusBackend.Tao) { // … } ``` ## Create a window ```kotlin import dev.nucleusframework.application.DecoratedWindow import dev.nucleusframework.application.NucleusBackend import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.window.TitleBar import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.rememberWindowState fun main() = nucleusApplication(backend = NucleusBackend.Tao) { DecoratedWindow( onCloseRequest = ::exitApplication, state = rememberWindowState(size = DpSize(1024.dp, 720.dp)), title = "Hello Tao", ) { TitleBar { state -> /* custom title bar */ } MyContent() } } ``` `DecoratedWindow` and `TitleBar` are the same Composables across backends, so your UI code does not change when you switch backends. For full-window layouts and custom chrome on Tao, use [`WindowScaffold`](/docs/tao/window-scaffold). To composite decoder or engine frames as GPU textures inside Compose (z-order, clipping, no CPU copy), use [`TextureView`](/docs/tao/texture-view). For in-process renderers that can share the scene's GPU device, use [`TaoGpuRenderContext`](/docs/tao/gpu-render-context). ## How it works Tao opens a native OS window directly: an `NSWindow` on macOS, an `HWND` on Windows, and a `wl_surface` (or an X11 window) on Linux. Compose renders into that surface through Skiko — Metal on macOS, ANGLE GLES over Direct3D 11 on Windows, EGL/GLES on Linux. On Linux the EGL attach asks for a GL 3.3 compatibility context, then a 3.3 core context, then Mesa llvmpipe (`LIBGL_ALWAYS_SOFTWARE=1`) so GPUs that only offer core 3.3 still hardware-accelerate, and machines with neither still open a window. Nothing routes through Swing's event-dispatch thread. Compose Desktop's `SwingPanel` does not work on the Tao backend and crashes the app at runtime. It requires an AWT/Swing window hierarchy that Tao never creates. Embed native content with [`NativeView`](/docs/tao/native-views) instead, or keep the AWT backend for windows that still need Swing interop. For passive GPU frames (video, cameras, engines), use [`TextureView`](/docs/tao/texture-view) rather than a native widget. The OS event loop runs on the process main thread, so the macOS launcher needs `-XstartOnFirstThread`. The Nucleus Gradle plugin adds it automatically for `run`, packaged distributions, and Compose Hot Reload. GraalVM native-image builds start on the main thread by default. The `nucleusApplication { }` entry point hides the backend choice from UI code. On the Tao backend the same window exposes an imperative `TaoWindow` handle underneath, which you reach through `nucleusWindow.unsafe.taoWindow` when you need to call into the native crate directly. ## API reference The Tao-specific API lives in `dev.nucleusframework.window.tao`: - `taoApplication { }` — direct entry point. Prefer `nucleusApplication` for backend portability. - `TaoWindow` — imperative window handle. Reach it through `LocalTaoWindow` or `nucleusWindow.unsafe.taoWindow`. - `NativeView` — embed any native view (`NSView` / `HWND` / `GtkWidget`) in Compose. - `TextureView` / `rememberTaoGpuRenderContext` — external GPU frames and in-process GPU renderers on the scene's device. See [TextureView](/docs/tao/texture-view) and [GPU render context](/docs/tao/gpu-render-context). - `MacOSStyle` — window chrome on macOS: `Auto` (modern Tahoe chrome on macOS 26+, classic chrome otherwise), `Classic` (the default), or `Modern`. - `TaoCursorIcon`, `TaoTrackpadGesture`, `TaoTrackpadPhase`, `TaoMouseButton`, `TaoEventCode` — input constants. - `TaoDeepLinkBridge.setSink { uri -> … }` — deep link delivery. - `WindowExceptionHandlerFactory` / `LocalWindowExceptionHandlerFactory` — catch scene failures, or let the default factory take the native fatal dialog. See [Handle uncaught exceptions](/docs/tao/decorated-window#handle-uncaught-exceptions). `decorated-window-jbr` and `decorated-window-jni` are legacy and maintenance-only: they still ship and still work, but new features land on Tao only. Keep one on the classpath and select the backend with `NucleusBackend.Awt` only if you still need Swing / AWT interop during migration. See [migration from JBR](/docs/tao/migration-from-jbr). ## What's next - [The decorated window](/docs/tao/decorated-window) — the `DecoratedWindow` and `TitleBar` Composables on Tao. - [Window scaffold and chrome](/docs/tao/window-scaffold) — full-window layouts, materials, drag areas. - [Multi-touch and gestures](/docs/tao/multi-touch) — trackpad and touch events from `TaoWindow`. - [Wayland](/docs/tao/wayland) — native Wayland behavior and detection. - [Quickstart](/docs/start/quickstart) — build and run your first Nucleus app. --- # DecoratedWindow on Tao URL: https://nucleusframework.dev/en/docs/tao/decorated-window 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 ```kotlin title="build.gradle.kts" plugins { id("dev.nucleusframework") } dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` `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`: ```kotlin 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`](/docs/tao/window-scaffold) 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: ```kotlin 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`: ```kotlin 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](/docs/tao/overlay-windows#build-a-click-through-watermark). | | `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](/docs/window/context-menu#turn-on-the-native-menu). | | `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`](/docs/window/context-menu#turn-on-the-native-menu). 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: ```kotlin 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](/docs/tao/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](/docs/os/tray-app#hide-the-dock-icon-on-macos). ### Scope members ```kotlin interface NucleusDecoratedWindowScope : DecoratedWindowScope { val nucleusWindow: NucleusWindow } interface NucleusWindow { val isFocused: Boolean val isMinimized: Boolean val isMaximized: Boolean val isFullscreen: Boolean val focusFlow: StateFlow 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](/docs/tao/window-scaffold). `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: ```kotlin 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 - [Window scaffold and chrome](/docs/tao/window-scaffold) — full-window layouts and materials. - [The Tao backend](/docs/tao) — what the no-AWT backend covers. - [DecoratedWindow across backends](/docs/window) — the shared window API. - [Multi-touch and trackpad gestures](/docs/tao/multi-touch) — read Tao input events. - [Backends](/docs/concepts/backends) — how Nucleus picks Tao or AWT. --- # Window scaffold and chrome URL: https://nucleusframework.dev/en/docs/tao/window-scaffold 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): ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` Existing `DecoratedWindow` + `TitleBar` call sites keep working. The scaffold is additive. ## Quickstart ```kotlin 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 | Placement | Behaviour | |---|---| | `TitleBarPlacement.Docked` | Classic 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 ```kotlin 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: ```kotlin 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: ```kotlin 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 ```kotlin 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: ```kotlin 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: ```kotlin 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: ```kotlin 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 ```kotlin 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: ```kotlin 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](/docs/tao/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 - [DecoratedWindow on Tao](/docs/tao/decorated-window) — window parameters and scope members. - [Window & toolkits](/docs/window) — shared window API and toolkit themes. - [Accessibility](/docs/tao/accessibility) — Compose semantics projected to VoiceOver, Narrator, Orca. --- # Overlay windows URL: https://nucleusframework.dev/en/docs/tao/overlay-windows Build watermarks, desktop widgets, and heads-up overlays with transparent, click-through, always-on-bottom, and all-workspaces windows on the Tao backend. An overlay is a window that sits outside the normal window stack: a watermark that ignores clicks, a widget pinned below every other window, a badge that follows the user across workspaces. This page covers the window flags that make those shapes possible and what each one does per platform. ## Add the dependency The flags are parameters on `DecoratedWindow` in `nucleus-application`, implemented by the Tao backend: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` Overlay flags are a Tao feature. On the AWT backends they are accepted and ignored, so the same source runs there with an ordinary window. ## Build a click-through watermark A passive overlay is transparent, undecorated, unfocusable, and lets the pointer pass through to whatever is behind it: ```kotlin import androidx.compose.ui.Alignment import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.rememberWindowState import dev.nucleusframework.application.DecoratedWindow import dev.nucleusframework.application.nucleusApplication fun main(args: Array) = nucleusApplication(args) { val state = rememberWindowState( position = WindowPosition.Aligned(Alignment.BottomEnd), size = DpSize(360.dp, 160.dp), ) DecoratedWindow( onCloseRequest = { }, state = state, title = "Watermark", resizable = false, undecorated = true, transparent = true, alwaysOnTop = true, focusable = false, clickThrough = true, hiddenFromDock = true, visibleOnAllWorkspaces = true, forceX11 = true, ) { Watermark() } } ``` Pair `clickThrough = true` with `focusable = false`: a window that cannot be clicked should not take keyboard focus either. ## Pin a widget below other windows A desktop widget is the same shape with the stacking inverted — `alwaysOnBottom` instead of `alwaysOnTop`, and clicks kept so the widget stays interactive: ```kotlin DecoratedWindow( onCloseRequest = ::exitApplication, state = rememberWindowState( position = WindowPosition.Aligned(Alignment.BottomEnd), size = DpSize(300.dp, 340.dp), ), title = "Nucleus widget", resizable = false, undecorated = true, transparent = true, hiddenFromDock = true, nativeContextMenu = true, visibleOnAllWorkspaces = true, forceX11 = true, alwaysOnBottom = true, ) { WidgetSurface() } ``` `alwaysOnTop` and `alwaysOnBottom` are mutually exclusive: setting one clears the other, and the last one set wins. Below-stacking is not desktop membership. The window still appears in the taskbar and Alt+Tab unless you also set `hiddenFromDock`, and it is not a true desktop-type window. On native Wayland a surface glued to the wallpaper needs `wlr-layer-shell`, which Tao does not implement. ## Toggle flags at run time `clickThrough`, `alwaysOnBottom`, and `visibleOnAllWorkspaces` are reactive parameters: change the state they read and the window follows. For imperative control, call the setters on the `TaoWindow` handle: ```kotlin import dev.nucleusframework.window.tao.TaoWindow fun applyOverlayMode(window: TaoWindow, passive: Boolean) { window.setIgnoreCursorEvents(passive) window.setAlwaysOnBottom(passive) window.setVisibleOnAllWorkspaces(passive) } ``` `transparent` and `forceX11` are creation-time only — they describe the native surface, which cannot change after the window exists. ## Take an X11 surface on Wayland Client-side stacking, positioning, and workspace stickiness do not exist in the Wayland protocols, so overlay flags are no-ops on a native Wayland surface. `forceX11 = true` gives one window an XWayland surface instead, without pushing the rest of the app off Wayland: ```kotlin DecoratedWindow(onCloseRequest = { }, forceX11 = true, /* … */) { Watermark() } ``` Read back what the window actually got: ```kotlin import dev.nucleusframework.core.runtime.Platform import dev.nucleusframework.window.tao.TaoWindow fun surfaceLabel(window: TaoWindow?): String? { if (window == null || Platform.Current != Platform.Linux) return null return if (window.isNativeWaylandSurface) "Wayland" else "X11" } ``` `isNativeWaylandSurface` reads the live native surface rather than the environment, so a `forceX11` window reports `false` while its siblings on Wayland report `true`. It is meaningful only once the window is ready. To move the whole process onto XWayland instead, set `NUCLEUS_TAO_LINUX_RENDERER=x11` (or `GDK_BACKEND=x11`) before launch. When a flag lands on a native Wayland surface, Nucleus logs a warning once per window and per feature through `java.util.logging`, naming the flag and the reason it cannot work. If `forceX11` finds no X server on `DISPLAY` — a session with no XWayland — the window stays on Wayland and says so. ## Platform behavior **macOS** `clickThrough` sets `NSWindow.ignoresMouseEvents`. `alwaysOnBottom` uses a window level below normal, and `visibleOnAllWorkspaces` joins every Space through `NSWindowCollectionBehaviorCanJoinAllSpaces`. A transparent window keeps its shadow, because AppKit shapes the shadow to the drawn content. Floating above another app's full-screen Space needs a window level Tao does not expose. **Windows** `clickThrough` applies `WS_EX_TRANSPARENT | WS_EX_LAYERED` and initializes the layer's alpha, so per-pixel transparency keeps working. `alwaysOnBottom` uses `HWND_BOTTOM`. `visibleOnAllWorkspaces` is a no-op and is not needed: a window hidden from the taskbar (`hiddenFromDock = true`) is not tracked by the Virtual Desktop Manager and already shows on every desktop. A fully transparent window drops the system shadow and the 1px DWM border, which trace the rectangular window rather than your content. Hit-testing is unaffected. **Linux** `clickThrough` installs an empty GDK input region. `alwaysOnBottom` maps to `_NET_WM_STATE_BELOW` and `visibleOnAllWorkspaces` to `gtk_window_stick()` — both X11 and XWayland only. Transparency itself works on both backends. Stacking, positioning, and workspace flags need X11: use `forceX11` per window, or `NUCLEUS_TAO_LINUX_RENDERER=x11` for the process. ## API reference ### DecoratedWindow parameters | Parameter | Default | Reactive | Description | |---|---|---|---| | `transparent` | `false` | No | Builds the native surface with an alpha channel; opaque background and title-bar colors are cleared. | | `clickThrough` | `false` | Yes | The window ignores pointer events; they reach whatever is behind it. | | `alwaysOnBottom` | `false` | Yes | Keeps the window below normal windows. Clears `alwaysOnTop`. | | `visibleOnAllWorkspaces` | `false` | Yes | Shows the window on every workspace or Space. | | `forceX11` | `false` | No | Linux only: request an X11 (XWayland) surface for this window. | `alwaysOnBottom` is also available on `HostedWindow`. `transparent`, `clickThrough`, `visibleOnAllWorkspaces`, and `forceX11` are additionally exposed on the Material and Jewel window wrappers. ### TaoWindow members | Member | Description | |---|---| | `setIgnoreCursorEvents(ignore: Boolean)` | Imperative `clickThrough`. | | `setAlwaysOnBottom(alwaysOnBottom: Boolean)` | Imperative `alwaysOnBottom`. | | `setVisibleOnAllWorkspaces(visible: Boolean)` | Imperative `visibleOnAllWorkspaces`. | | `isNativeWaylandSurface: Boolean` | Whether this window ended up on a native Wayland surface. | Reach the handle from the window scope with `nucleusWindow.unsafe.taoWindow`. ## Notes - A mostly transparent window looks better with custom chrome than with the stock `TitleBar`; combine `transparent = true` with `undecorated = true` for a borderless overlay. - Compose scenes are told the window is transparent, so dialog scrims and popups blend against the alpha background instead of painting over a grey sheet. - `WindowPosition.Aligned` cannot be honored on native Wayland — the compositor decides placement. Nucleus logs this once per process. ## What's next - [DecoratedWindow on Tao](/docs/tao/decorated-window) — the rest of the window parameters. - [Native Wayland](/docs/tao/wayland) — session detection and the X11 fallback. - [Context menus](/docs/window/context-menu) — the menu an interactive widget needs. --- # Native views on Tao URL: https://nucleusframework.dev/en/docs/tao/native-views Embed any platform-native view — NSView, HWND, or GtkWidget — 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` (`NSView*`, `HWND`, or `GtkWidget*`), and Nucleus reparents that handle into the Tao window, keeps it sized to the Compose layout slot, and disposes it when the composable leaves. Anything that exposes one of those handles works — AppKit/SwiftUI views, Win32 or WinUI controls, WebView2, GTK widgets, and so on. ## Add the dependency ```kotlin title="build.gradle.kts" plugins { id("dev.nucleusframework") } dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` `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: ```kotlin 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` 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. From 2.5.15, a trackpad pan over a `NativeView` is consumed and forwarded (`PanStart` / `PanMove` / `PanEnd`) so the embedded scroll view rubber-bands; the Compose scrollable above does not open a competing pan session. 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, in the host window's own Compose scene on all three platforms — the same hole-punch blending path as later Compose siblings and in-scene popups. Overlay content claims its own input; anything it does not cover falls through to the native view, with no marker modifier needed. ## API reference ### Describe the native view `NucleusPlatformView` is a sealed interface. Implement exactly one platform variant and expose the corresponding handle as a `Long`: | Variant | Platform | Handle property | |---|---|---| | `NucleusPlatformView.NsView` | macOS | `nsViewHandle` — pointer to your `NSView*` | | `NucleusPlatformView.HWnd` | Windows | `hwndHandle` — pointer to your `HWND` | | `NucleusPlatformView.GtkWidget` | Linux | `gtkWidgetHandle` — pointer to your `GtkWidget*` | The interface also declares lifecycle callbacks with default no-op bodies. Override the ones your view needs: | Member | Called 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 ```kotlin @Composable fun NativeView( factory: () -> NucleusPlatformView, modifier: Modifier = Modifier, update: (NucleusPlatformView) -> Unit = {}, cornerRadius: Dp = Dp.Unspecified, content: @Composable () -> Unit = {}, ) ``` | Parameter | Notes | |---|---| | `factory` | Creates the `NucleusPlatformView`. Runs once; the result is remembered and disposed when the composable leaves. | | `modifier` | Sizes and positions the layout slot. The embedded view fills it. | | `update` | Runs on every recomposition with the remembered view. Use it to push new state into the native view. | | `cornerRadius` | Rounded-corner clip; see below. | | `content` | Compose overlay rendered on top of the native view. | ### Overlay Compose content The `content` slot draws Compose UI over the embedded view, in the window's own Compose scene. Anything you put there is an ordinary composable: it takes pointer events where it is drawn, and events it does not consume are replayed to the native view underneath. ```kotlin NativeView( factory = { WebPlatformView(createNativeWebView(url)) }, modifier = Modifier.fillMaxSize(), ) { // A Compose toolbar over the web view. The button handles its own clicks; // clicks elsewhere reach the web view underneath. Button(onClick = { /* reload */ }) { Text("Reload") } } ``` Because the overlay lives in the main scene, Compose siblings declared after the `NativeView` also paint over it, and `Popup`s from inside the scene land above the native view. `Modifier.consumeOverlayPointerEvents()` is deprecated and no longer needed — overlay content claims its own input. The `cursor` argument still maps to `Modifier.pointerHoverIcon`; use that modifier directly instead. The native view is composited into the scene with a punched hole: Compose clears the view's rectangle to transparent, and the platform shows the native content through it. **macOS** The `NSView` is inserted below the Tao content view, and the Metal layer becomes non-opaque so Skia clears at alpha 0. Moves are applied in the same frame as the Compose geometry. **Windows** The child `HWND` is parented under the window. Win32 children always paint above their parent, so Compose is re-rendered on top through a DirectComposition overlay clipped to the union of the live `NativeView` rectangles — Compose still sees events first inside those rectangles. A `hwndHandle` of `0L` is legal: a view that has no Win32 child window (WebView2 in composition mode, for example) drives its own visual through `setBounds` and `setCornerRadius`. **Linux** The `GtkWidget` is reparented under the window and the GL surface paints above it, so the punched hole reveals the widget. While a `NativeView` is attached, the surface advertises an empty opaque region so the compositor keeps blending the GTK content underneath. This requires Wayland. On the X11 backend a child window's alpha is never blended against its parent, so the punched rectangle shows the desktop instead of the widget — the widget still runs, but behind the GL surface. ### 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: ```kotlin 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. `cornerRadius` has no effect on Linux, where GTK 3 has no per-widget rounded clipping. Draw a Compose `RoundedCornerShape` border in the `content` slot instead. ## What's next - [WebView](/docs/webview) — ready-made multiplatform WebView built on this primitive for desktop. - [TextureView](/docs/tao/texture-view) — external GPU textures in the Compose scene (no CPU copy). - [The Tao backend](/docs/tao) — what the no-AWT backend covers. - [DecoratedWindow on Tao](/docs/tao/decorated-window) — open the window that hosts the view. - [Multi-touch and trackpad gestures](/docs/tao/multi-touch) — read Tao input events. --- # TextureView URL: https://nucleusframework.dev/en/docs/tao/texture-view Composite an externally produced GPU texture into the Compose scene on the Tao backend without a CPU frame copy. `TextureView` composites an externally produced GPU texture inside the Compose scene on the Tao backend. Z-order, clipping, `Modifier.graphicsLayer` transforms, and scrolling apply to the texture the same way they do to any other composable. There is no CPU frame copy: the producer texture is imported on the window's GPU device (ANGLE shared D3D11 on Windows, Metal/`IOSurface` on macOS, DMA-BUF/`EGLImage` on Linux). It is the passive GPU-pixels counterpart of [`NativeView`](/docs/tao/native-views). Prefer `NativeView` for interactive native widgets; use `TextureView` when you already own a decoder or renderer that publishes GPU frames (video, camera, game engine, GPU compute). For **in-process** renderers that can allocate on the scene's own GPU device, use [`TaoGpuRenderContext`](/docs/tao/gpu-render-context) instead of importing a foreign texture. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` `TextureView`, `TextureViewController`, and the platform source factories live in `dev.nucleusframework.window.tao`. ## Show a texture 1. Obtain a platform handle from your producer (shared DXGI handle, `IOSurface` pointer, DMA-BUF fd, …). 2. Wrap it with the matching factory into a `TextureViewSource`. 3. Place a `TextureView` and, for live content, a `TextureViewController`. 4. After each published frame, call `controller.markFrameAvailable()` (any thread). Only the draw pass re-runs — no recomposition. ```kotlin import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.layout.ContentScale import dev.nucleusframework.window.tao.TextureView import dev.nucleusframework.window.tao.TextureViewController import dev.nucleusframework.window.tao.TextureViewSource import dev.nucleusframework.window.tao.rememberTextureViewController @Composable fun VideoPane(source: TextureViewSource?) { val controller = rememberTextureViewController() DisposableEffect(source) { // Start your producer loop; call controller.markFrameAvailable() after each frame. onDispose { /* stop producer */ } } TextureView( source = source, controller = controller, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, filterQuality = FilterQuality.Low, ) } ``` `filterQuality` and `contentScale` mirror Compose `Image`. Content outside the composable bounds is clipped. When `source` is `null`, does not match the host platform, or the import fails, `TextureView` renders an empty `Box(modifier)`. ## Platform sources Factories return a `TextureViewSource`. Pass only the factory that matches the OS you run on; other platforms treat an unmatched source as an empty box. **Windows** ```kotlin import dev.nucleusframework.window.tao.nucleusD3D11SharedTextureSource val source = nucleusD3D11SharedTextureSource( sharedHandle = dxgiSharedHandle, // IDXGIResource::GetSharedHandle (legacy) widthPx = width, heightPx = height, ) ``` - Handle must be a **legacy** DXGI shared handle (`GetSharedHandle`). NT handles (`D3D11_RESOURCE_MISC_SHARED_NTHANDLE`) are not supported by ANGLE's import path. - Texture format: `R8G8B8A8_UNORM` (or compatible RGBA8) with premultiplied alpha; `widthPx` / `heightPx` must match the D3D texture size. - Synchronization: - `D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX` (recommended) — each `markFrameAvailable` pulls under `AcquireSync(0)` / `ReleaseSync(0)` into a staging texture (tear-free, one GPU-GPU copy). Bracket producer writes the same way. - plain `D3D11_RESOURCE_MISC_SHARED` — true zero-copy; the producer must `Flush()` after each frame or a redraw may sample a partial frame. The [mediafoundation-demo](https://github.com/NucleusFramework/Nucleus/tree/v2.5.18/examples/mediafoundation-demo) sample shows Media Foundation decode into a shared D3D11 texture. **macOS** ```kotlin import dev.nucleusframework.window.tao.nucleusIOSurfaceTextureSource import dev.nucleusframework.window.tao.nucleusMetalTextureSource // Shareable buffer (cross-device / cross-process) val surfaceSource = nucleusIOSurfaceTextureSource( ioSurface = ioSurfaceRefAsLong, widthPx = width, heightPx = height, ) // Same-process Metal texture (or CVMetalTextureCache output via its IOSurface) val metalSource = nucleusMetalTextureSource( metalTexture = mtlTextureAsLong, widthPx = width, heightPx = height, ) ``` - Pixel format: 32-bit `BGRA` or `RGBA` (sRGB variants included), premultiplied alpha. - Surface / texture dimensions must match `widthPx` × `heightPx`. - Each frame is pulled with one GPU-GPU copy on the window's command queue. Finish producer writes (`commit` + `waitUntilCompleted`, or double-buffer) **before** `markFrameAvailable`. - A texture that is neither render-target-capable on the window's Metal device nor `IOSurface`-backed cannot be imported; hand over an `IOSurface` source instead. The [avfoundation-demo](https://github.com/NucleusFramework/Nucleus/tree/v2.5.18/examples/avfoundation-demo) sample decodes with AVFoundation/VideoToolbox into an `IOSurface`. **Linux** ```kotlin import dev.nucleusframework.window.tao.NucleusDmaBufPlane import dev.nucleusframework.window.tao.NucleusDrmFormat import dev.nucleusframework.window.tao.NucleusYuvColorSpace import dev.nucleusframework.window.tao.NucleusYuvFormat import dev.nucleusframework.window.tao.nucleusDmaBufTextureSource import dev.nucleusframework.window.tao.nucleusEglImageTextureSource import dev.nucleusframework.window.tao.nucleusYuvDmaBufTextureSource // Packed 32-bit RGB DMA-BUF val rgb = nucleusDmaBufTextureSource( fd = dmaBufFd, widthPx = width, heightPx = height, stride = stride, fourcc = NucleusDrmFormat.ARGB8888, offset = 0, modifier = NucleusDrmFormat.MODIFIER_INVALID, ) // Producer-owned EGLImage on the window's EGLDisplay val egl = nucleusEglImageTextureSource(eglImage = eglImageAsLong, widthPx = width, heightPx = height) // Planar YUV (I420 / YV12) — hardware decoder native layout val yuv = nucleusYuvDmaBufTextureSource( widthPx = width, heightPx = height, format = NucleusYuvFormat.I420, planes = listOf( NucleusDmaBufPlane(fd = yFd, stride = yStride, offset = 0), NucleusDmaBufPlane(fd = uFd, stride = uStride, offset = 0), NucleusDmaBufPlane(fd = vFd, stride = vStride, offset = 0), ), colorSpace = NucleusYuvColorSpace.BT709_LIMITED, ) ``` - Single-plane RGB uses DRM FourCC codes in `NucleusDrmFormat` (`ARGB8888`, `XRGB8888`, `ABGR8888`, `XBGR8888`). The driver maps sampling to RGBA for app code. - `fd` stays owned by the caller; EGL takes its own reference at import time. - YUV supports **three-plane** 4:2:0 layouts only (`I420`, `YV12`). NV12/NV21 are not supported yet (Skia has no two-channel GPU texture mapping for interleaved chroma). - For acquire fences instead of a CPU-side finish, call `controller.markFrameAvailable(acquireFenceFd)` (Linux DMA-BUF only; requires `EGL_ANDROID_native_fence_sync`). Elsewhere the fd is ignored and remains yours. The [gstreamer-demo](https://github.com/NucleusFramework/Nucleus/tree/v2.5.18/examples/gstreamer-demo) sample feeds GStreamer frames as an `EGLImage`. ## How it works `TextureView` is a pure drawing surface. The producer owns its device, queue, and allocation; Nucleus only imports that allocation onto the window's GPU and samples it during Compose's draw pass. Several `TextureView`s that share one `TextureViewSource` share a single GPU import. Frame signalling goes through `TextureViewController`: - `markFrameAvailable()` increments an internal stamp that invalidates **draw only**. - Safe from any thread (decoder thread, render thread, pool). - Optional Linux overload `markFrameAvailable(acquireFenceFd)` transfers ownership of a `sync_file` fd; the consumer GPU waits before sampling. Pass `TextureViewController.NO_FENCE` (`-1`) for no fence. - `rememberTextureViewController()` releases any held fence when composition leaves. Input is not handled. Interactive native UI still uses `NativeView`. Test helpers (`D3D11TestTextureProducer`, `MetalTestTextureProducer`, `DmaBufTestTextureProducer`) ship in `decorated-window-tao` for demos and CI; production apps use their own producers and the public factories above. ## API reference ### TextureView ```kotlin @Composable fun TextureView( source: TextureViewSource?, modifier: Modifier = Modifier, controller: TextureViewController? = null, filterQuality: FilterQuality = FilterQuality.Low, contentScale: ContentScale = ContentScale.FillBounds, alignment: Alignment = Alignment.Center, ) ``` ### TextureViewController ```kotlin class TextureViewController { fun markFrameAvailable() fun markFrameAvailable(acquireFenceFd: Int) companion object { const val NO_FENCE: Int = -1 } } @Composable fun rememberTextureViewController(): TextureViewController ``` ### Source factories | Factory | Platform | Handle | |---------|----------|--------| | `nucleusD3D11SharedTextureSource(sharedHandle, widthPx, heightPx)` | Windows | Legacy DXGI shared handle | | `nucleusIOSurfaceTextureSource(ioSurface, widthPx, heightPx)` | macOS | `IOSurfaceRef` as `Long` | | `nucleusMetalTextureSource(metalTexture, widthPx, heightPx)` | macOS | `id` as `Long` | | `nucleusDmaBufTextureSource(fd, widthPx, heightPx, stride, …)` | Linux | Single-plane DMA-BUF | | `nucleusEglImageTextureSource(eglImage, widthPx, heightPx)` | Linux | Producer-owned `EGLImageKHR` | | `nucleusYuvDmaBufTextureSource(widthPx, heightPx, format, planes, colorSpace)` | Linux | Planar YUV DMA-BUF | Supporting types: `NucleusDrmFormat`, `NucleusDmaBufPlane`, `NucleusYuvFormat` (`I420`, `YV12`), `NucleusYuvColorSpace` (`BT601_*`, `BT709_*`). ## Notes - **Tao only.** There is no AWT/`SwingPanel` path for external GPU textures. - Import failure is silent at the UI level (empty box). Check producer logs and that dimensions / formats match the contract above. - Do not call `markFrameAvailable` faster than the display can composite without also pacing the producer — the stamp coalesces, but wasted GPU work does not. - Tray panels and popup layers keep their EGL binding neutral so a `TextureView` inside a standalone tray panel still imports correctly. ## What's next - [GPU render context](/docs/tao/gpu-render-context) — in-process renderers on the scene's device. - [Native views](/docs/tao/native-views) — embed interactive `NSView` / `HWND` / `GtkWidget` content. - [DecoratedWindow on Tao](/docs/tao/decorated-window) — open windows that host `TextureView`. - [Tao backend overview](/docs/tao) — backends, Wayland, and related primitives. --- # GPU render context URL: https://nucleusframework.dev/en/docs/tao/gpu-render-context Give in-process GPU renderers the scene's Skia DirectContext and native device so they can composite without a second GPU or per-frame copy. `TaoGpuRenderContext` publishes the GPU context of the enclosing Compose surface on the Tao backend: the Skia `DirectContext` the scene draws with, plus the native device behind it (Metal on macOS, ANGLE GLES on Windows, native EGL/GLES on Linux). An in-process renderer — a map engine, 3D viewport, or charting library — allocates its render targets **on that same device** and lets Compose sample them with no second GPU, no shareable handle, and no per-frame cross-device copy. It is the complement of [`TextureView`](/docs/tao/texture-view), not a replacement. `TextureView` is the contract for **foreign** producers (out-of-process engines, hardware decoders on their own device). Use `TaoGpuRenderContext` when your renderer can create its GPU objects on the scene's context. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` Types live in `dev.nucleusframework.window.tao`. ## Obtain the context Call `rememberTaoGpuRenderContext()` inside a surface that owns a GPU context (`DecoratedWindow` content, a native popup layer, a `NativeView` overlay, or a tray panel). It returns `null` during bring-up, teardown, or a non-hardware environment: ```kotlin import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.withFrameNanos import dev.nucleusframework.window.tao.TaoGpuRenderContext import dev.nucleusframework.window.tao.TaoOpenGlRenderContext import dev.nucleusframework.window.tao.rememberTaoGpuRenderContext @Composable fun MapViewport() { val renderContext = rememberTaoGpuRenderContext() ?: return // Key renderer state on the instance: a rebuilt context is a new identity. val renderer = remember(renderContext) { MapEngineRenderer(renderContext) } DisposableEffect(renderer) { onDispose { renderer.close() } } LaunchedEffect(renderer) { while (true) { // GPU work inside withFrameNanos — see Frame pacing below. withFrameNanos { renderer.renderFrame() } } } // … sample the engine output in Compose } ``` ## Backends | Type | Platforms | Extra API | |------|-----------|-----------| | `TaoMetalRenderContext` | macOS | `metalDevicePtr` — borrowed `id` | | `TaoOpenGlRenderContext` | Linux, Windows | `withContextCurrent { }` — bind the surface GL context | Both expose: | Member | Description | |--------|-------------| | `backend: TaoRenderBackend` | `METAL` or `OPENGL` | | `skiaContext: DirectContext` | Skia context the scene paints with (borrowed) | | `runOnGpuThread { }` | Exclusive access for Skia / GPU work (Metal hops to the render thread; GL runs inline on the event-loop thread after a thread check) | On Windows, `OPENGL` is ANGLE-on-D3D11 GLES — not desktop OpenGL. Resolve entry points through ANGLE's `eglGetProcAddress`, the same way Skia does for the scene. ## Ownership and lifetime Every handle is **borrowed**. Do not retain, release, or free them. Stop using a context the moment the surface publishes a different one (or `null`). `rememberTaoGpuRenderContext()` returns a **new instance** whenever the underlying context is rebuilt — window detach, a Wayland hide/show cycle (full EGL stack rebuild), popup or tray panel teardown. Key renderer state on that instance: ```kotlin val renderer = remember(renderContext) { EngineRenderer(renderContext) } DisposableEffect(renderContext) { onDispose { renderContext.runOnGpuThread { renderer.releaseGpuResources() } } } ``` ## Frame pacing Issue GPU work **inside** the `withFrameNanos` callback (or during composition / layout / draw), not in the code that runs after the suspension: - On Linux the host hands the EGL context to a swap thread for blocking `eglSwapBuffers` right after every frame. A post-frame continuation races that hand-off and gets `withContextCurrent → null` almost every vsync. - The frame callback runs inside the render pass, while the context is bindable. When the scene has live `TextureView` imports or `TaoGpuRenderContext` consumers, Tao keeps VSync on through the Windows modal move/resize loop and Linux resize bursts so frame-clock-driven content stays display-paced instead of running at event-pump speed. ## TextureView vs GPU render context | | `TextureView` | `TaoGpuRenderContext` | |--|---------------|------------------------| | Producer | Foreign / out-of-process | In-process, same device | | Import | Shared D3D11 / IOSurface / DMA-BUF | Allocate on `skiaContext` | | Typical use | Video decoder, game engine, camera | MapLibre-style engines, 3D viewports, charts | ## Notes - Call every member from the composition thread (Tao event-loop thread; process main thread on macOS). Hop GPU work through `runOnGpuThread` / `withContextCurrent`. - Nested `withContextCurrent` is safe; the previous current context is restored afterwards and Skia's GL state cache is invalidated so consumers do not need to. - Each Windows window host owns an unshared ANGLE GLES context. Resize-path GPU cache purges bind that host's context first so a sibling window's textures stay intact. macOS and Linux popup / tray surfaces may also own private contexts. ## What's next - [TextureView](/docs/tao/texture-view) — import external GPU frames into the scene. - [Native views](/docs/tao/native-views) — embed interactive native widgets. - [DecoratedWindow](/docs/tao/decorated-window) — host Tao windows from composition. - [The Tao backend](/docs/tao) — architecture and backend selection. --- # Multi-touch & gestures URL: https://nucleusframework.dev/en/docs/tao/multi-touch Read trackpad and touch gestures on the Tao backend through Compose pointer events, or the raw gesture stream. The Tao backend forwards the operating system's gesture stream into Compose pointer events, so pinches, rotations, and trackpad scrolls reach your UI through the standard Compose input APIs. When you need the unprocessed gesture data, `TaoWindow` also exposes the raw trackpad stream. ## Add the dependency Gesture support ships with the Tao backend. No extra artifact is required. ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` ## Read gestures from Compose Most gesture handling goes through Compose. Trackpad and touch input arrive as `PointerInputChange` values, so `detectTransformGestures` and the other pointer-input modifiers work without any Tao-specific code: ```kotlin import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput @Composable fun ZoomCanvas() { var scale by remember { mutableStateOf(1f) } Box( Modifier .fillMaxSize() .pointerInput(Unit) { detectTransformGestures { _, _, zoom, _ -> scale *= zoom } }, ) } ``` Each `PointerInputChange` also carries the input source and pressure, which lets you branch on how the event was produced: ```kotlin import androidx.compose.ui.input.pointer.PointerType Modifier.pointerInput(Unit) { awaitPointerEventScope { while (true) { val change = awaitPointerEvent().changes.first() val source = change.type // PointerType.Mouse | Touch — Tao emits no Stylus/Eraser val pressure = change.pressure // 1f when the digitizer reports none // React to source / pressure. } } } ``` ## Read trackpad pans from Compose From 2.5.15 on macOS, two-finger trackpad gestures arrive as `PointerEventType.PanStart` / `PanMove` / `PanEnd` with `panOffset` in pixels. Mouse-wheel notches stay `Scroll`. Foundation's `Modifier.scrollable` handles both; a custom `pointerInput` that only listens for `Scroll` will not see the trackpad: ```kotlin import androidx.compose.ui.input.pointer.PointerEventType Modifier.pointerInput(Unit) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() val change = event.changes.first() when (event.type) { PointerEventType.PanMove -> { val pan = change.panOffset // pixels; AWT preciseWheelRotation × 10.dp // Apply the pan. } PointerEventType.Scroll -> { val scroll = change.scrollDelta // Wheel notch or phase-less precise scroll. } else -> Unit } } } } ``` Start the process with `-Dnucleus.tao.trackpadPanEvents=false` to receive AWT-style `Scroll` events for every step instead. The pan stays open across AppKit's inertial momentum tail. Closing it when the fingers lift would stack Compose's own fling on top of the platform animation. A `PanEnd` arrives after the tail, or after a short grace if no tail follows. ## Read the raw gesture stream To distinguish gesture kinds and phases directly — for example, telling a pinch apart from a rotate — register a `TrackpadGestureListener` on the current `TaoWindow`: ```kotlin import dev.nucleusframework.window.tao.LocalTaoWindow import dev.nucleusframework.window.tao.TaoTrackpadGesture import dev.nucleusframework.window.tao.TaoTrackpadPhase val window = LocalTaoWindow.current window?.onTrackpadGesture { kind, phase, xFixed, yFixed, valueFixed -> if (kind == TaoTrackpadGesture.MAGNIFY && phase == TaoTrackpadPhase.CHANGED) { val ratio = valueFixed / 10_000f // magnify delta as a ratio // Apply the zoom delta. } } ``` The callback receives fixed-point integers: `xFixed` and `yFixed` are view-relative physical pixels multiplied by 1024, and `valueFixed` is the delta multiplied by 10000 (a ratio for magnify, degrees for rotate). `onTrackpadGesture` replaces the previously registered listener rather than adding one. Register a single listener per window. ## How it works On each platform, Tao subscribes to the native high-resolution gesture source — `NSEvent` magnify and rotate on macOS, precision-touchpad input on Windows, and the Wayland pointer-gesture protocol on Linux. The Rust bridge reshapes those events and delivers them to the JVM through a single wire format, which the scene host turns into Compose pointer events. The raw `TrackpadGestureListener` sits on that same wire, before the pointer-event synthesis. macOS and Linux emit magnify, rotate, and smart-magnify; Windows emits magnify only. There is no native source on other configurations, so the listener does not fire there. Tao reports cursor positions in 1/1024 px, so from 2.5.6 a move under 1 dp from the last dispatched position is suppressed before it reaches Compose. Without that deadband, a click drifting a fraction of a pixel between press and release starts a parent drag gesture, which consumes the change and cancels the tap. Press, release, exit, and scroll dispatch at the last dispatched position; motion past the deadband keeps its exact sub-pixel position. From the same release, `viewConfiguration.touchSlop` is density-scaled to 18.dp — AWT and Android parity — instead of 18 raw pixels, which halved every drag and scroll threshold on 2x displays. From 2.5.15, macOS trackpad scrolling matches AWT: AppKit already follows the `MouseScrollDelta` convention, so the extra negation of `scrollingDeltaX` is gone, and precise deltas are handed over in logical points (AWT never applies the display scale). Phased steps go through `TaoTrackpadPanRouter` and become Pan events at `10.dp` per AWT wheel unit — the same factor Compose Desktop's `MacOSCocoaConfig` applies to a notch. Wheel notches and phase-less precise scrolls stay Scroll. Native views consume the pan and forward it so the embedded scroll view rubber-bands; popups share the same router as the window. ## API reference ### Register a gesture listener ```kotlin // dev.nucleusframework.window.tao.TaoWindow fun onTrackpadGesture(listener: TrackpadGestureListener) fun interface TrackpadGestureListener { fun onGesture( kind: Int, // TaoTrackpadGesture value phase: Int, // TaoTrackpadPhase value xFixed: Int, // physical pixels × 1024, view-relative yFixed: Int, valueFixed: Int, // delta × 10000 (ratio for magnify, degrees for rotate) ) } ``` Obtain the window from `LocalTaoWindow.current`, which is `null` outside a Tao window. ### Gesture constants ```kotlin object TaoTrackpadGesture { const val MAGNIFY: Int = 0 const val ROTATE: Int = 1 const val SMART_MAGNIFY: Int = 2 } object TaoTrackpadPhase { const val BEGAN: Int = 0 const val CHANGED: Int = 1 const val ENDED: Int = 2 const val CANCELLED: Int = 3 } ``` ### Pointer info on every change ```kotlin // androidx.compose.ui.input.pointer.PointerInputChange val type: PointerType // Mouse | Touch | Stylus | Eraser val pressure: Float // 0f when the device reports no pressure val position: Offset // sub-pixel position val scrollDelta: Offset // scroll wheel and phase-less precise scroll val panOffset: Offset // trackpad pan (macOS, 2.5.15+); pixels ``` ## What's next - [Pen & stylus](/docs/tao/pen-stylus) — pressure and tilt from stylus devices. - [Wayland](/docs/tao/wayland) — Linux-specific input and windowing behavior. - [Decorated window](/docs/tao/decorated-window) — the Tao window and its title bar. - [Tao backend](/docs/tao) — what the Tao backend provides. --- # Pen & stylus input URL: https://nucleusframework.dev/en/docs/tao/pen-stylus Read pen and touchscreen pressure from Compose pointer events on the Tao backend. The Tao backend delivers pen, stylus, and touchscreen contacts as Compose pointer events of type `PointerType.Touch`, each carrying a `pressure` value. You read them with the same `pointerInput` modifier you use for mouse input. The only pen-specific field is `pressure`. ## Add the dependency Pen and touch support ships with the Tao backend. There is no separate artifact to add. ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` ## Read pressure from pointer events Read pointer events inside a `pointerInput` modifier. Keep the `PointerType.Touch` contacts — this is what pen and touchscreen input arrives as on Tao — and use `change.pressure` to size each stroke: ```kotlin import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.* @Composable fun InkSurface() { val strokes = remember { mutableStateListOf>() } var last: Offset? by remember { mutableStateOf(null) } Canvas( Modifier .fillMaxSize() .pointerInput(Unit) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() val change = event.changes.first() if (change.type != PointerType.Touch) { last = null continue } val start = last ?: change.position strokes += Triple(start, change.position, change.pressure) last = if (change.pressed) change.position else null change.consume() } } } ) { strokes.forEach { (a, b, p) -> drawLine(Color.Black, a, b, strokeWidth = 1f + p * 6f) } } } ``` ## How it works Tao reads each operating system's native pointer input and forwards every contact into the Compose scene. On Windows, pen and touchscreen input arrives through `WM_POINTER` / `WM_TOUCH`; on Linux, through the Wayland and X11/XWayland touch protocols. Each contact is dispatched as a `PointerType.Touch` pointer, so at the Compose layer a pen is indistinguishable from a finger except by its pressure. The native layer reports pressure as an integer scaled by 10,000, which Tao normalizes to `change.pressure` in the `[0f, 1f]` range. When the digitizer does not report pressure — the case for most consumer touchscreens — the contact still reports `1f`. A `1f` reading therefore means either full pressure or no pressure data, not strictly maximal pressure. Because a contact surfaces as an ordinary `PointerInputChange`, everything that works for mouse and touch — hit testing, gesture detectors, `consume()` — works for pen input unchanged. ## API reference ### Pointer fields ```kotlin val change = event.changes.first() val type: PointerType = change.type // Mouse | Touch val pressure: Float = change.pressure // in [0f, 1f] val pressed: Boolean = change.pressed val position: Offset = change.position ``` `PointerType` and `PointerInputChange` are standard Compose types from `androidx.compose.ui.input.pointer`. The Tao backend reports pen and touch contacts as `PointerType.Touch`; it does not produce `PointerType.Stylus` or `PointerType.Eraser`. `PointerInputChange` carries no tilt or rotation, so those are not available through the event. ### Platform support | Platform | Native source | Pressure | |-----------------------------|------------------------------|-------------------------------------------| | Windows | `WM_POINTER` / `WM_TOUCH` | Reported when the digitizer supports it. | | Linux (Wayland, X11/XWayland) | touch protocol | Reported when the digitizer supports it. | | macOS | — | No touch path; contacts arrive as mouse. | ## Notes A contact reports `pressure = 1f` both at full pressure and when the digitizer exposes no pressure at all. Treat `1f` as "maximal or unavailable", not strictly maximal. ## What's next - [Multi-touch and trackpad gestures](/docs/tao/multi-touch) — read touch and trackpad input on Tao. - [Wayland](/docs/tao/wayland) — Linux compositor requirements for touch input. - [The Tao backend](/docs/tao) — how the no-AWT windowing backend fits together. --- # Drag and drop URL: https://nucleusframework.dev/en/docs/tao/drag-and-drop Receive dropped files on the Tao backend through the standard Compose dragAndDropTarget API. The Tao backend routes native drag-and-drop into Compose through the standard `Modifier.dragAndDropTarget` API. When something is dropped on your window, `DragAndDropEvent.nativeEvent` is a `TaoDragAndDropPayload` carrying the dropped file paths, and `event.awtTransferable` resolves the same data through the usual `DataFlavor` path — without initializing the AWT toolkit. Inbound drops carry files only. Text, HTML, and URL payloads are not delivered yet. ## Add the dependency Drag and drop ships with the Tao backend. Add the backend artifact to your build: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` No extra configuration is required. Once a window runs on the Tao backend, drop targets receive events. ## Receive dropped files Attach a `DragAndDropTarget` to a composable with `Modifier.dragAndDropTarget` and read the files from the Tao payload in `onDrop`: ```kotlin import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draganddrop.DragAndDropEvent import androidx.compose.ui.draganddrop.DragAndDropTarget import dev.nucleusframework.window.tao.TaoDragAndDropPayload @Composable fun DropZone() { val target = remember { object : DragAndDropTarget { override fun onDrop(event: DragAndDropEvent): Boolean { val payload = event.nativeEvent as? TaoDragAndDropPayload val files = payload?.files ?: emptyList() files.forEach { path -> println("dropped: $path") } return files.isNotEmpty() } } } Box( Modifier .fillMaxSize() .dragAndDropTarget( shouldStartDragAndDrop = { true }, target = target, ), ) } ``` `TaoDragAndDropPayload.files` is a `List` of absolute paths. `onDrop` returns `true` when your target accepts the data. If you already consume drops through the portable Compose Desktop path, keep using `event.awtTransferable` and `DataFlavor.javaFileListFlavor` — the Tao backend backs it with the same paths, so that code works unchanged. ## How it works There is no AWT `DropTarget` on the input path. The native window reports a drop through the Rust JNI bridge, and the Tao Compose scene host builds a `DragAndDropEvent` for it. The event's `nativeEvent` is a `TaoDragAndDropPayload`, and the host also wraps the paths in an internal `Transferable` so `event.awtTransferable` keeps working. That adapter lives in the `java.datatransfer` layer and never starts the AWT toolkit. Inbound routing (a drop into your window) is wired on each platform host, including standalone tray popups (`TrayApp`) from 2.5.3 on Windows and macOS and from 2.5.4 on Linux. Those popups are ownerless native panels, not `TaoWindow`s; file drops still arrive through `Modifier.dragAndDropTarget`. Outbound routing (a drag that leaves your window) is handled by an internal `PlatformDragAndDropManager` and is opt-in per host: where a platform has no outbound launcher yet, the gesture is reported as not started. Both paths increment counters on `TaoDnDDiagnostics`. From 2.3, starting a drag from Compose `dragAndDropSource` no longer freezes the Tao host for the duration of the OS modal drag loop. Windows (`DoDragDrop`), macOS (AppKit), and Linux (GTK) all pump frames through a shared `DragPump` on the native side so Compose keeps painting while the cursor is outside the window. ## API reference ### Read a dropped payload `TaoDragAndDropPayload` is the value you get from `DragAndDropEvent.nativeEvent` on the Tao backend. You never construct it yourself. | Member | Type | Description | |----------------|----------------|--------------------------------------| | `files` | `List` | Absolute paths of the dropped files. | ### Inspect the DnD plumbing `TaoDnDDiagnostics` is an object exposing Compose state counters and a recent-message log. Read the counters inside a composable to observe the plumbing; they update as events flow. | Member | Type | Description | |--------------------|------------------------|---------------------------------------------------------| | `constructed` | `MutableIntState` | Number of drag-and-drop managers constructed. | | `isRequiredQueries`| `MutableIntState` | Times Compose queried whether an outbound transfer is required. | | `requests` | `MutableIntState` | Outbound transfer requests received. | | `transfers` | `MutableIntState` | Outbound transfers started. | | `lastMessage` | `MutableState`| Most recent diagnostic message, or `null`. | | `log(msg: String)` | `Unit` | Records a diagnostic message. | ```kotlin import androidx.compose.material.Text import dev.nucleusframework.window.tao.TaoDnDDiagnostics @Composable fun DnDStatus() { Text("transfers: ${TaoDnDDiagnostics.transfers.intValue}") TaoDnDDiagnostics.lastMessage.value?.let { Text(it) } } ``` ## What's next - [The Tao backend](/docs/tao) — the no-AWT window backend that delivers these events. - [Multi-touch](/docs/tao/multi-touch) — trackpad gestures on the same backend. - [Decorated window](/docs/tao/decorated-window) — windows, title bars, and native handles. --- # Accessibility URL: https://nucleusframework.dev/en/docs/tao/accessibility The Tao accessibility bridge projects your Compose semantics tree onto the native accessibility API of each operating system. The Tao backend exposes your UI to assistive technology by walking Compose's semantics tree and projecting it onto the native accessibility API of each operating system: NSAccessibility on macOS, and AccessKit on both Windows (UI Automation provider) and Linux (AT-SPI). From 2.2, Windows and Linux share the same AccessKit wire and decode path. You author accessibility with standard Compose semantics; the bridge does the projection. The `TaoA11yNode`, `TaoA11yRole`, `TaoA11yFlag`, and `TaoA11yAction` types in this module describe what the bridge emits, so you can inspect or assert on the projected tree. ## Add the dependency The bridge ships inside the Tao backend — there is no separate artifact. Add `decorated-window-tao` and the projection is installed per window automatically. ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` The public accessibility types live in `dev.nucleusframework.window.tao`. ## Make your UI accessible You do not build accessibility nodes by hand. Attach Compose semantics to your composables and the bridge translates them into native accessibility nodes: ```kotlin import androidx.compose.foundation.Image import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics Button(onClick = { count++ }) { Text("Increment") } Image( painter = painterResource("logo.png"), contentDescription = "Company logo", ) Text( "Loading", modifier = Modifier.semantics { contentDescription = "Loading, please wait" }, ) ``` The `Button` is projected as a `TaoA11yRole.Button` node that advertises the `TaoA11yAction.CLICK` action; the `Image` becomes a `TaoA11yRole.Image` node with its `contentDescription` as the label. A screen reader reads these directly with no extra wiring. Nodes marked `InvisibleToUser` or `HideFromAccessibility` in Compose are pruned before projection, so they never reach the OS accessibility tree. ## How it works Each decorated window installs a `SemanticsOwnerListener` when it is created. When the semantics tree changes, the backend walks the `SemanticsOwner`, produces a flat list of `TaoA11yNode` values, serializes them to a compact binary wire format, and pushes them to the native side over JNI. Frame coordinates are window-local logical points with a top-left origin, matching Compose; the native side converts to each platform's coordinate space on demand. The walk is debounced and gated. When no assistive technology is attached, the backend skips the walk entirely so scrolling and animation stay smooth; the first query from a screen reader activates the projection and seeds the tree. On Linux, subsequent updates are sent as partial snapshots that carry only the changed nodes. On macOS every update is a full snapshot. Windows uses the AccessKit UIA provider (with live PropertyChanged for out-of-process .NET clients); updates follow the same AccessKit tree path as Linux. Actions travel back the other way. When VoiceOver, Narrator, or Orca invokes an action such as a click, an increment, or a text edit, the native side calls back into the JVM, which resolves the target node and runs the matching Compose `SemanticsActions` handler on the UI thread. This is why authoring standard Compose semantics is sufficient — the action set advertised on each node (`TaoA11yAction`) is derived from the semantics actions you declared. ## API reference All types are in `dev.nucleusframework.window.tao`. ### Inspect a projected node `TaoA11yNode` is the descriptor the bridge produces for a single element. It is a `data class`, so equality drives the partial-update diff on Linux. ```kotlin data class TaoA11yNode( val nodeId: Long, val parentId: Long, val role: TaoA11yRole, val flags: Int, // bitmask of TaoA11yFlag val extraFlags: Int = 0, // bitmask of TaoA11yExtraFlag val actions: Int, // bitmask of TaoA11yAction val frameX: Float, val frameY: Float, val frameW: Float, val frameH: Float, val minValue: Float = 0f, val maxValue: Float = 0f, val numericValue: Float = 0f, val selectionStart: Int = 0, val selectionEnd: Int = 0, val hScrollMax: Float = 0f, val hScrollValue: Float = 0f, val vScrollMax: Float = 0f, val vScrollValue: Float = 0f, val label: String = "", val valueString: String = "", val customActions: List = emptyList(), val testTag: String = "", val children: List = emptyList(), ) ``` `label` is the accessible name and `valueString` its spoken value. `customActions` holds the labels of Compose custom accessibility actions, dispatched back by index. `testTag` carries the Compose `Modifier.testTag(...)` value; it maps to `AXIdentifier` on macOS, `UIA_AutomationIdPropertyId` on Windows, and AccessKit's author id on Linux, so UI automation can find widgets by a stable identifier instead of by visible label. ### Identify the node kind `TaoA11yRole` is the projected element role. Compose roles and node kinds map onto these values. ```kotlin enum class TaoA11yRole(val code: Int) { Unknown, Group, Button, StaticText, Checkbox, RadioButton, Switch, TextField, TextArea, Slider, Progress, Image, ScrollArea, Heading, Tab, PopupMenu, Table, Outline, Row, Cell, SpinButton, TabPanel, Tooltip, } ``` ### Read node state `TaoA11yFlag` holds the state bits packed into `TaoA11yNode.flags`. Test them with a bitwise `and`. ```kotlin if (node.flags and TaoA11yFlag.CHECKED != 0) { /* the element is checked */ } ``` | Flag | Meaning | |---|---| | `IS_ELEMENT` | Node is an accessibility element, not a pure container. | | `ENABLED` | Element is enabled. | | `FOCUSED` | Element currently holds accessibility focus. | | `SELECTED` | Element is selected. | | `CHECKED` | Toggle/checkbox is on. | | `MIXED` | Toggle is in the indeterminate state. | | `HEADING` | Element is a heading. | | `PASSWORD` | Text field is a password field. | | `MULTILINE` | Text field accepts multiple lines. | | `MODAL` | Container is modal. | | `LIVE_REGION_POLITE` | Live region announced politely. | | `LIVE_REGION_ASSERTIVE` | Live region announced assertively. | | `MULTI_SELECTABLE` | Container allows multiple selection (Linux/AT-SPI only). | | `EXPANDED_TRUE` | Expandable element is expanded (Linux/AT-SPI only). | | `EXPANDED_FALSE` | Expandable element is collapsed (Linux/AT-SPI only). | | `HIDDEN` | Reserved; not currently emitted. | `TaoA11yExtraFlag` carries additional bits in `TaoA11yNode.extraFlags`. | Flag | Meaning | |---|---| | `READ_ONLY` | Text field is read-only; the `SET_TEXT` action is dropped. | | `INVALID` | Form field holds an invalid value. | ### Check supported actions `TaoA11yAction` holds the action bits packed into `TaoA11yNode.actions`. Each bit corresponds to a Compose semantics action the element declared, and to the callback the native side may invoke. ```kotlin if (node.actions and TaoA11yAction.CLICK != 0) { /* the element can be activated */ } ``` | Action | Triggered by | |---|---| | `CLICK` | Activating the element. | | `INCREMENT` | Stepping a slider or spin button up. | | `DECREMENT` | Stepping a slider or spin button down. | | `SET_TEXT` | Replacing the text of an editable field. | | `REQUEST_FOCUS` | Moving accessibility focus to the element. | | `SCROLL_UP` | Scrolling the region up. | | `SCROLL_DOWN` | Scrolling the region down. | | `SCROLL_LEFT` | Scrolling the region left. | | `SCROLL_RIGHT` | Scrolling the region right. | | `DISMISS` | Dismissing a popup or dialog. | ## What's next - [Decorated window](/docs/tao/decorated-window) — the Tao window and its title bar. - [The Tao backend](/docs/tao) — what the Tao backend provides. - [Wayland](/docs/tao/wayland) — Linux windowing behavior, where AT-SPI is bridged through AccessKit. --- # Native Wayland URL: https://nucleusframework.dev/en/docs/tao/wayland The Tao backend renders Compose Desktop apps directly on Wayland and falls back to X11 automatically. On Linux, the Tao backend renders Compose Desktop apps directly on Wayland through native Wayland protocols, rather than through the XWayland compatibility layer. When no Wayland session is available, it falls back to X11 without any code change. This page covers what the Wayland path supports, how it works, and how to read the current session from Kotlin. ## Add the dependency Wayland support ships with the Tao backend. There is no separate artifact. ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` ## Detect the session You rarely branch on the session type — the same Compose code runs on Wayland and X11. Detection is useful for diagnostics and feature gates, since taskbar progress, badges, and global hotkeys behave differently per session. ```kotlin import dev.nucleusframework.core.runtime.LinuxDesktopEnvironment import dev.nucleusframework.core.runtime.Platform val onWayland = Platform.isWayland val desktopEnv = if (Platform.Current == Platform.Linux) LinuxDesktopEnvironment.Current else null ``` ## How it works Tao binds the standard Wayland protocols at startup: `xdg-shell` for windows, `xdg-decoration` to negotiate server- or client-side decorations, `wp_fractional_scale_v1` for sub-integer scale factors, `pointer-gestures-unstable-v1` for pinch and swipe, and `wp_tablet_v2` for stylus input. Skiko renders to an EGL surface attached to the window's `wl_surface`. When the compositor signals a scale change — for example, when you drag a window from a 1.0 monitor to a 1.5 monitor — Tao reconfigures the EGL surface and emits a density change to Compose. Your `Dp` values stay logical and the rendered output stays crisp. See [Per-monitor HiDPI](/docs/tao/hidpi) for the details. On an X11 session, or on a Wayland compositor that refuses XDG decorations, Tao falls back to X11 automatically. The Compose code does not change. ## Wayland and X11 differences | Concern | Wayland (Tao) | X11 (fallback) | |----------------------|------------------------------------|----------------------------------| | Fractional scaling | Native, per-output | Whole-pixel only | | Window placement | Compositor-controlled | Client can set absolute position | | Global hotkeys | Compositor-mediated (`xdg-portal`) | `XGrabKey` | | Screen capture | Portal only | Direct | | Pen pressure | `wp_tablet_v2` | Not exposed | | Multi-touch gestures | `pointer-gestures-unstable-v1` | Limited | | Clipboard | `wl_data_device` on the window's seat | `CLIPBOARD` selection | | Window stacking | No client-side protocol | `_NET_WM_STATE_ABOVE` / `BELOW` | ## Copy and paste On Linux, Tao reads and writes the clipboard through GDK rather than AWT. AWT's clipboard is X11-only, so in a Wayland-native window it pasted nothing — the Wayland selection reaches XWayland only while an X11 window is active — and it threw outright in a session with no XWayland at all. Use the standard Compose API; there is nothing Nucleus-specific to call: ```kotlin import androidx.compose.ui.platform.LocalClipboard val clipboard = LocalClipboard.current val scope = rememberCoroutineScope() scope.launch { clipboard.setClipEntry(entry) } ``` Plain text, images, and file lists are carried in both directions. Images cross as PNG, and file lists as `text/uri-list` plus the GNOME copied-files target, so pasting into a file manager works. HTML is not carried in either direction, and only the `CLIPBOARD` selection is used — never X11's PRIMARY (middle-click) selection. ## Notes On Wayland you cannot set absolute screen coordinates — the compositor owns window placement. `WindowState.position` is treated as a hint by some compositors and ignored by others (GNOME ignores client-requested positions). Design around `Center` or `PlatformDefault`, and persist the last size rather than the last position. ## API reference - `Platform.isWayland` — `true` on a Linux session when `XDG_SESSION_TYPE` is `wayland` or `WAYLAND_DISPLAY` is set. - `Platform.Current` — one of `Linux`, `Windows`, `MacOS`, or `Unknown`. - `LinuxDesktopEnvironment.Current` — one of `Gnome`, `KDE`, `XFCE`, `Cinnamon`, `Mate`, or `Unknown`. ## What's next - [Per-monitor HiDPI](/docs/tao/hidpi) — how fractional scaling maps to Compose density. - [Multi-touch and gestures](/docs/tao/multi-touch) — pinch, swipe, and rotate on Wayland. - [Pen and stylus](/docs/tao/pen-stylus) — pressure and tilt through `wp_tablet_v2`. --- # Per-monitor HiDPI URL: https://nucleusframework.dev/en/docs/tao/hidpi The Tao backend tracks a per-window scale factor so Compose layouts stay in Dp while rendering matches each display's pixel density. The Tao backend tracks the scale factor of each window's current display and reconfigures its render surface when that factor changes. Your Compose layout stays in `Dp`; only the rasterized output changes to match the display's pixel density. Per-monitor HiDPI is part of `decorated-window-tao` and needs no extra setup. ## Read the current density Read `LocalDensity` inside any composable to get the scale factor of the window it renders in: ```kotlin import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalDensity @Composable fun ShowDensity() { val density = LocalDensity.current Text("Scale: ${density.density}x · font scale: ${density.fontScale}") } ``` The value updates automatically when the window moves to a display with a different scale factor. ## How it works When you open a `DecoratedWindow` on Tao, the backend queries the active display for its scale factor and configures the Skiko surface accordingly: `CAMetalLayer.contentsScale` on macOS, an ANGLE GL surface resize at the new scale on Windows, and the EGL buffer scale on Wayland. The native window receives a `ScaleFactorChanged` event whenever you drag it between displays, plug in an external monitor, or change the system scale. Tao forwards that event to the `ComposeScene`, which propagates a new `LocalDensity` to your composables. The result: a `Modifier.size(48.dp)` occupies 48 device pixels on a 1.0 display, 96 device pixels on a 2.0 display, and 72 device pixels on a 1.5 fractional Wayland output. The layout does not change — only the rasterized output does. ## linux-hidpi and the AWT backends The `linux-hidpi` module reads a single global scale from the desktop environment (GSettings, Mutter, `GDK_SCALE`, or `Xft.dpi`) and applies it to JVM properties before AWT initializes, through `applyLinuxHiDpiScale()`. It sets one scale at startup and does not react to per-window changes. The Tao backend does not use that helper — it observes per-window scale changes at runtime. When you migrate from the AWT backends to Tao on Linux, you can drop the `linux-hidpi` dependency. Keep `linux-hidpi` only for sessions where you still run the AWT (JBR or JNI) backends. ## API reference - `LocalDensity.current` — the Compose density of the window the composable renders in. - `Platform.isWayland` — `true` on a Wayland session, where fractional scaling is per-monitor; `false` on X11. - `WindowState` — hold window sizes in `Dp`, never in pixels, so they survive a scale change. ## Notes - On Windows 10 1607 and later, Tao declares Per-Monitor V2 DPI awareness through the application manifest. The Nucleus Gradle plugin writes that manifest for packaged builds. Popups and `DecoratedDialog` pick up the target monitor's DPI and work-area constraints when they open on a different display from the owner. - On macOS, Retina backing stores are configured automatically. - Fractional scaling on KDE Plasma works on both X11 and Wayland, but only Wayland reports per-monitor accuracy. ## What's next - [Tao backend](/docs/tao) — the no-AWT native windowing backend. - [Wayland](/docs/tao/wayland) — session detection and Wayland-specific behavior. - [Decorated window](/docs/tao/decorated-window) — the window you render your UI into. --- # Taskbar progress URL: https://nucleusframework.dev/en/docs/tao/taskbar-progress Show determinate, indeterminate, and error progress on the taskbar or dock from a Tao window. `taskbar-progress-tao` bridges the platform-neutral `TaskbarProgress` API to the Tao backend so a `TaoWindow` can drive its taskbar button (Windows), dock icon (macOS), or launcher entry (Linux). It adds a composable handle bound to the current Tao window and a backend-agnostic façade that works whether your app runs on the Tao or AWT backend. Use this module when you build against the Tao backend and want to reflect a long-running operation — a download, an export, a build — in the OS taskbar. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.taskbar-progress-tao:") } ``` The module depends on `taskbar-progress` and `decorated-window-tao`, so both are pulled in transitively. ## Show progress from a composable Inside a Tao `DecoratedWindow` content lambda, call `rememberTaoTaskbarProgress()` to get a handle bound to the current window, then drive it from your state: ```kotlin import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import dev.nucleusframework.taskbarprogress.tao.rememberTaoTaskbarProgress // Inside DecoratedWindow { ... } var progress by remember { mutableStateOf(0.0) } val taskbar = rememberTaoTaskbarProgress() LaunchedEffect(progress) { taskbar?.showProgress(progress) if (progress >= 1.0) taskbar?.hideProgress() } ``` `rememberTaoTaskbarProgress()` returns a `TaoTaskbarProgressScope?`. It is `null` when called outside a Tao `DecoratedWindow`, so guard the calls with `?.`. The `value` passed to `showProgress` and `setProgress` is a fraction in the `0.0..1.0` range. Progress is a visual hint. `isAvailable()` reports `false` on platforms or environments without taskbar-progress support (for example, a Linux desktop that exposes no matching `.desktop` file), and the calls become no-ops. ## How it works `TaoTaskbarProgress` resolves the native window handle from the `TaoWindow` before each call. On Windows it reads the HWND from `TaoWindow.nativeHandle`. On macOS the dock icon is application-wide and on Linux the launcher entry is keyed by the `.desktop` filename, so the window argument is not used on those platforms and the call delegates straight to the matching platform branch. `rememberTaoTaskbarProgress()` reads `LocalTaoWindow` and remembers a `TaoTaskbarProgressScope` keyed to that window. The scope is a thin wrapper: each of its methods forwards to `TaoTaskbarProgress` with the bound window, so you never pass the window explicitly. `NucleusTaskbarProgress` is the backend-agnostic entry point. It takes a `NucleusWindow` and dispatches to the AWT `TaskbarProgress` or to `TaoTaskbarProgress` depending on how the window is backed, which lets a project switch backends without touching call sites. Every call is offloaded to a dedicated daemon thread, because the underlying Windows API can block the caller if invoked while the owning event loop is shutting down. As a result the returned `Boolean` reports best-effort dispatch (queued or not possible), not completion. ## API reference ### Check availability ```kotlin TaoTaskbarProgress.isAvailable(): Boolean ``` Returns `true` when the current platform supports taskbar progress. Check it once before wiring up progress if you want to skip the work entirely on unsupported systems. ### Show determinate progress From a `TaoTaskbarProgressScope`: ```kotlin scope.showProgress(value: Double) // sets NORMAL state, then the value scope.setProgress(value: Double) // updates the value only scope.setState(state: TaskbarProgress.State) ``` `showProgress` sets the state to `NORMAL` and applies the fraction in one call. Use `setProgress` to update the value while the state stays the same. `TaskbarProgress.State` has `NO_PROGRESS`, `INDETERMINATE`, `NORMAL`, `ERROR`, and `PAUSED`. ### Signal indeterminate, error, or paused ```kotlin scope.showIndeterminate() // pulsing bar, no fixed value scope.showError(value: Double = 1.0) scope.showPaused(value: Double = 1.0) ``` `showIndeterminate` shows a pulsing bar for work with no measurable fraction. `showError` and `showPaused` set the matching state and a full bar by default; pass a `value` to keep the current fraction visible. ### Clear progress ```kotlin scope.hideProgress() // sets NO_PROGRESS ``` Call this when the operation finishes to remove the overlay. ### Request user attention ```kotlin scope.requestAttention(type: TaskbarProgress.AttentionType = TaskbarProgress.AttentionType.INFORMATIONAL) scope.stopAttention() ``` `requestAttention` flashes the taskbar button on Windows or bounces the dock icon on macOS. `AttentionType` is `INFORMATIONAL` (a brief flash or single bounce) or `CRITICAL` (flash or bounce until the app gets focus). `stopAttention` cancels an ongoing request. ### Drive progress without a composable When you hold a `TaoWindow` directly, call the `TaoTaskbarProgress` object and pass the window: ```kotlin TaoTaskbarProgress.showProgress(window, 0.5) TaoTaskbarProgress.hideProgress(window) ``` The object exposes the same operations as the scope: `setProgress`, `setState`, `showProgress`, `showError`, `showIndeterminate`, `showPaused`, `hideProgress`, `requestAttention`, and `stopAttention`. ### Stay backend-agnostic If your code works with a `NucleusWindow` and should run on either backend, use `NucleusTaskbarProgress` or its `NucleusWindow` extensions: ```kotlin import dev.nucleusframework.taskbarprogress.tao.showTaskbarProgress import dev.nucleusframework.taskbarprogress.tao.hideTaskbarProgress nucleusWindow.showTaskbarProgress(0.5) nucleusWindow.hideTaskbarProgress() ``` The extensions mirror the scope: `setTaskbarProgress`, `setTaskbarState`, `showTaskbarProgress`, `showTaskbarError`, `showTaskbarIndeterminate`, `showTaskbarPaused`, `hideTaskbarProgress`, `requestTaskbarAttention`, and `stopTaskbarAttention`. Each returns a `Boolean` reporting best-effort dispatch, not completion. ## What's next - [Taskbar progress (AWT)](/docs/lifecycle/taskbar-progress) — the platform-neutral `TaskbarProgress` API and the AWT backend. - [The Tao backend](/docs/tao) — the no-AWT Rust window backend and its window model. - [Decorated windows on Tao](/docs/tao/decorated-window) — the `DecoratedWindow` that provides `LocalTaoWindow`. --- # Migrate from JBR to Tao URL: https://nucleusframework.dev/en/docs/tao/migration-from-jbr Switch an existing Nucleus app from the AWT backend to the Tao backend and retest the platform integrations that change. In this tutorial, you'll move an existing Nucleus app from the AWT backend (`decorated-window-jbr` or `decorated-window-jni`) to the Tao backend (`decorated-window-tao`). The `DecoratedWindow` and `TitleBar` APIs are identical on both backends, so the move is a dependency swap and a backend flag. The rest of the tutorial covers the integrations you need to retest afterward. On the same machine, running the same app as a GraalVM native image, moving from the AWT backend to Tao measurably cuts both startup time and memory: - **Startup** (launch → first window): **~0.7 s → ~0.12 s**, roughly **6× faster**. - **RAM** (private working set, idle with window open): **~155 MB → ~100 MB**, about **35% lower**. Tao drops the AWT/Swing window hierarchy and its native libraries entirely — that is what removes the startup and memory overhead. Figures are from an internal Nucleus app; your results vary with app size and platform. ## Before you start - An existing app that runs on `NucleusBackend.Awt`, using `decorated-window-jbr` or `decorated-window-jni`. - JDK 17 or later. - Familiarity with the [backend concept](/docs/concepts/backends): a Nucleus app ships exactly one window runtime module, and `NucleusBackend` selects it. ## Swap the dependency Replace the AWT window runtime with `decorated-window-tao`. Keep the `nucleus.nucleus-application` entry point unchanged. **Before (AWT)** ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-jbr:") } ``` **After (Tao)** ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` ## Switch the backend flag Pass `backend = NucleusBackend.Tao` to `nucleusApplication`. The window content stays identical. **Before (AWT)** ```kotlin nucleusApplication { DecoratedWindow(onCloseRequest = ::exitApplication, title = "MyApp") { TitleBar { state -> /* … */ } MyContent() } } ``` **After (Tao)** ```kotlin nucleusApplication(backend = NucleusBackend.Tao) { DecoratedWindow(onCloseRequest = ::exitApplication, title = "MyApp") { TitleBar { state -> /* … */ } MyContent() } } ``` `NucleusBackend.Auto` (the default) picks Tao when `decorated-window-tao` is on the classpath and falls back to the AWT backend otherwise. Set the flag explicitly to pin the choice per build. ## Retest the platform integrations The APIs above are stable across backends, but the code paths underneath change. Retest each of these after the swap: - **Drag-and-drop.** Payloads arrive as `TaoDragAndDropPayload` instead of an AWT `TransferHandler`. Rewrite any handler that reads AWT transfer data. - **Deep links.** Register with `onDeepLink { uri -> … }` as before — the callback API is unchanged, only the bridge behind it differs (`TaoDeepLinkBridge` on Tao). - **File dialogs.** Tao has no AWT, so any direct `JFileChooser` call stops working. Switch to a native file dialog library such as FileKit — see [Native file dialogs](/docs/ecosystem/file-dialog), which runs on both backends. - **Native views.** Embedded AWT `Canvas` instances no longer work under Tao. Compose Desktop's `SwingPanel` also stops working and crashes the app at runtime, since Tao never creates the AWT/Swing window hierarchy it needs. Embed native content with `NativeView` instead. - **Window placement on Linux.** Wayland compositors may ignore absolute positions. See [Wayland](/docs/tao/wayland). - **Multi-window flows.** These are unchanged: one `nucleusApplication { }` block with one `DecoratedWindow` call per window, each exposing its own `NucleusWindow`. ## Compare the backends | Concern | AWT (JBR / JNI) | Tao | |------------------------|-------------------------------------|---------------------------------------| | Startup (native image) | ~0.7 s to first window | ~0.12 s to first window (~6× faster) | | RAM (idle, window open)| ~155 MB private working set | ~100 MB private working set (~35% lower) | | Underlying window | `ComposeWindow` (extends `JFrame`) | `TaoWindow` (native handle) | | Event dispatch | AWT EDT | Tao event loop on the main thread | | `unsafe` accessor | `unsafe.awtWindow` | `unsafe.taoWindow` | | Native file dialogs | AWT / Swing | [FileKit](/docs/ecosystem/file-dialog) (xdg portals / native panels) | | Drag-and-drop | AWT `TransferHandler` | `TaoDragAndDropPayload` | | Deep links | AWT / AppKit | `TaoDeepLinkBridge` (same `onDeepLink` API) | | Native embedding | AWT `Canvas` | `NativeView` | | Wayland | XWayland | Native | | macOS main thread | Not required | Requires `-XstartOnFirstThread` | ## Decide whether to keep JBR Stay on the AWT backend when your app: - ships a Swing-based module, - embeds an AWT `Canvas` you have not ported to `NativeView` yet, or - depends on a library that needs `ComposeWindow` directly. Nucleus publishes `decorated-window-jbr`, `decorated-window-jni`, and `decorated-window-tao` from the same version, so you can defer the switch and pin the backend per build with `NucleusBackend.Awt` or `NucleusBackend.Tao`. ## What's next - [Backends](/docs/concepts/backends) — how `NucleusBackend` selects the window runtime. - [Decorated window](/docs/tao/decorated-window) — the `DecoratedWindow` and `TitleBar` APIs on Tao. - [Native file dialogs](/docs/ecosystem/file-dialog) — file pickers that run without AWT. - [Wayland](/docs/tao/wayland) — Linux window placement and compositor behavior. --- # Window & toolkits URL: https://nucleusframework.dev/en/docs/window How Nucleus opens native desktop windows from Compose through the DecoratedWindow composable, the TitleBar slot, and the toolkit modules that restyle them. Nucleus opens native desktop windows from Compose through a single composable, `DecoratedWindow`. It draws the OS window frame, gives you a `TitleBar` slot you fill with Compose content, and hands back a `NucleusWindow` handle that behaves the same whether the Tao backend or the deprecated AWT backend runs underneath. This section covers the windowing entry point and the toolkit modules that change how it looks. ## The DecoratedWindow composable `DecoratedWindow` lives in `nucleus-application`. You pair it with one backend module and call it from inside `nucleusApplication { }`: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") } ``` ```kotlin import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.ui.Modifier 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.nucleusApplication fun main() = nucleusApplication { val state = rememberWindowState(size = DpSize(1100.dp, 720.dp)) DecoratedWindow( onCloseRequest = ::exitApplication, state = state, title = "Sample", minimumSize = DpSize(640.dp, 480.dp), ) { TitleBar { _ -> Text("My App", Modifier.padding(8.dp)) } Box(Modifier.fillMaxSize()) { /* app content */ } } } ``` Each call to `DecoratedWindow` manages its own state, handle, and close request, so you open a second window by calling it again. There are two overloads: one on `NucleusApplicationScope`, and a receiverless top-level one that resolves the scope from `LocalNucleusApplicationScope` — use that to open a window from a navigation destination or a row action, without threading the receiver through. Beyond the Compose Desktop parameters, `DecoratedWindow` takes the Nucleus window flags: `undecorated`, `popupFor`, `nativePopupLayers`, `hiddenFromDock`, [`nativeContextMenu`](/docs/window/context-menu#turn-on-the-native-menu), and the overlay flags `transparent`, `clickThrough`, `visibleOnAllWorkspaces`, `forceX11` and `alwaysOnBottom`. See [DecoratedWindow parameters](/docs/tao/decorated-window#decoratedwindow-parameters) and [Overlay windows](/docs/tao/overlay-windows). ## The title bar slot Inside the content lambda, `TitleBar` is a Compose composable rather than a native widget. Its content lambda receives the current `DecoratedWindowState`, so you can react to focus and maximized state, and place buttons, a search field, or any other content across the bar: ```kotlin TitleBar { state -> Text(if (state.isActive) "My App" else "My App (inactive)") } ``` `DecoratedDialog` is the dialog counterpart and pairs with `DialogTitleBar` for modal or detached panels. For full-window layouts, custom design-system chrome, macOS glass regions, or Windows 11 Mica / Acrylic, use [`WindowScaffold`](/docs/tao/window-scaffold) on the Tao backend. Existing `TitleBar` call sites stay valid; the scaffold is additive. ## The window handle The content lambda runs in a `NucleusDecoratedWindowScope`, which carries a `nucleusWindow` handle. Use it to read window state, observe changes, and drive the window without touching the native object: ```kotlin DecoratedWindow(onCloseRequest = ::exitApplication, title = "Editor") { TitleBar { _ -> Text("Editor") } val focused by nucleusWindow.focusFlow.collectAsState() Button(onClick = { nucleusWindow.setMaximized(true) }) { Text(if (focused) "Maximize" else "(unfocused)") } } ``` `NucleusWindow` exposes the intersection of `ComposeWindow` (deprecated AWT backend) and `TaoWindow` (Tao): the boolean properties `isFocused`, `isMinimized`, `isMaximized`, and `isFullscreen`; a matching `StateFlow` for each; `boundsOnScreen()`; and imperative calls such as `setMaximized`, `setMinimumSize`, `toFront`, and `close`. The same handle is available anywhere in the tree through `LocalNucleusWindow`. When you need the underlying native object, `nucleusWindow.unsafe` is the escape hatch. The accessor matching the active backend returns a value; the others return `null`: ```kotlin interface NucleusWindowUnsafe { val awtWindow: ComposeWindow? // null on Tao val awtDialog: ComposeDialog? val taoWindow: TaoWindow? // null on AWT val taoHandle: Long? } ``` ## Styling and toolkits The visual style — title bar height, colors, button icons — lives in `TitleBarStyle` and `DecoratedWindowStyle` in `decorated-window-core`. Wrap your app in `NucleusDecoratedWindowTheme` to apply a style directly, or add a toolkit module for a ready-made native look. The composable you call stays the same; only the theme changes. Native design systems are available today: [macOS 26](/docs/window/toolkit-macos), [Fluent](/docs/window/toolkit-fluent), and [Yaru](/docs/window/toolkit-yaru) ship as separate Maven artifacts with Nucleus-backed windows; Jewel and Material adapters live in this repository. See [Toolkits](/docs/window/toolkits). ## Backends `nucleusApplication { }` detects the active backend — Tao when `decorated-window-tao` is on the classpath, otherwise the deprecated AWT backend — and sets up the platform integrations before running your windows. `LocalNucleusBackend` reports the resolved value, one of `NucleusBackend.Auto`, `.Awt`, or `.Tao`. Because `nucleusWindow` mirrors both backends, the same window code runs on either. `nucleusApplication` acquires a single-instance lock by default. Pass `enableSingleInstance = false` to allow multiple processes. ## What's next - [Toolkits](/docs/window/toolkits) — change the window's look with a prebuilt native theme. - [Window scaffold and chrome](/docs/tao/window-scaffold) — full-window layouts, drag areas, materials. - [The Tao backend](/docs/tao/decorated-window) — the no-AWT windowing backend in detail. - [Backends](/docs/concepts/backends) — Tao and the deprecated AWT backend. --- # Native design systems URL: https://nucleusframework.dev/en/docs/window/toolkits macOS 26, Fluent, Yaru, Jewel and Material restyle the same Nucleus window for each desktop design language. Nucleus opens the OS window; design systems style the chrome and the widgets. macOS 26, Fluent and Yaru ship as separate open-source Compose Multiplatform libraries on Maven Central, each with a Nucleus-backed decorated window for desktop. Jewel and Material adapters stay inside the Nucleus repository. ## The toolkits | Toolkit | Native to | Artifacts / module | Status | |---------|-----------|--------------------|--------| | [macOS 26](https://github.com/NucleusFramework/compose-macos-26-ui) | macOS Tahoe | `dev.nucleusframework:compose-macos-ui` | Available (v1.0.0) | | [Fluent](https://github.com/NucleusFramework/compose-fluent-ui) | Windows 11 | `dev.nucleusframework.composefluent:fluent` + `decorated-window-fluent` | Available (v1.0.0) | | [Yaru](https://github.com/NucleusFramework/yaru-compose-ui) | Ubuntu / GNOME | `dev.nucleusframework.yarucompose:yaru` + `yaru-decorated-window` | Available (v1.0.1) | | Jewel | Cross-platform | `nucleus.decorated-window-jewel` | Available (with Nucleus) | | Material 2 | Fallback | `nucleus.decorated-window-material2` | Available (with Nucleus) | | Material 3 | Fallback | `nucleus.decorated-window-material3` | Available (with Nucleus) | The three design-system repos version **independently** of Nucleus. Pin them separately; they depend on Nucleus 2.2+ for the Tao window on desktop. Live galleries: - [compose-macos-26-ui](https://nucleusframework.github.io/compose-macos-26-ui/) - [compose-fluent-ui](https://nucleusframework.github.io/compose-fluent-ui/) - [yaru-compose-ui](https://nucleusframework.github.io/yaru-compose-ui/) ## Choose a toolkit **macOS 26** ```kotlin title="build.gradle.kts" plugins { id("dev.nucleusframework") } dependencies { implementation("dev.nucleusframework:compose-macos-ui:1.0.0") } ``` ```kotlin import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.macoscompose.window.MacosDecoratedWindow fun main() = nucleusApplication { MacosDecoratedWindow(onCloseRequest = ::exitApplication, title = "Mail") { // MacosTheme + components live inside the window content App() } } ``` See the [macOS 26 toolkit](/docs/window/toolkit-macos) page and the [repo README](https://github.com/NucleusFramework/compose-macos-26-ui). **Fluent** ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework.composefluent:fluent:1.0.0") implementation("dev.nucleusframework.composefluent:decorated-window-fluent:1.0.0") } ``` ```kotlin import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.window.fluent.FluentDecoratedWindow import dev.nucleusframework.window.fluent.FluentTitleBar import io.github.composefluent.FluentTheme fun main() = nucleusApplication { FluentTheme { FluentDecoratedWindow(onCloseRequest = ::exitApplication, title = "Settings") { FluentTitleBar { /* … */ } App() } } } ``` See the [Fluent toolkit](/docs/window/toolkit-fluent) page and [compose-fluent-ui](https://github.com/NucleusFramework/compose-fluent-ui). **Yaru** ```kotlin title="build.gradle.kts" dependencies { // Pulls yaru + Nucleus Tao transitively. implementation("dev.nucleusframework.yarucompose:yaru-decorated-window:1.0.1") } ``` ```kotlin import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.yarucompose.window.YaruDecoratedWindow fun main() = nucleusApplication { YaruDecoratedWindow(onCloseRequest = ::exitApplication, title = "Settings") { // YaruTheme must be inside the window content App() } } ``` See the [Yaru toolkit](/docs/window/toolkit-yaru) page and [yaru-compose-ui](https://github.com/NucleusFramework/yaru-compose-ui). **Jewel** ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") implementation("dev.nucleusframework:nucleus.decorated-window-jewel:") } ``` ```kotlin import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.darkmodedetector.isSystemInDarkMode import dev.nucleusframework.window.jewel.JewelDecoratedWindow import dev.nucleusframework.window.jewel.JewelTitleBar import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme fun main() = nucleusApplication { IntUiTheme(isDark = isSystemInDarkMode()) { JewelDecoratedWindow(onCloseRequest = ::exitApplication, title = "Tools") { JewelTitleBar { state -> /* … */ } MyContent() } } } ``` **Material 3** ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") implementation("dev.nucleusframework:nucleus.decorated-window-material3:") } ``` ```kotlin import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.window.material.MaterialDecoratedWindow import dev.nucleusframework.window.material.MaterialTitleBar fun main() = nucleusApplication { MaterialDecoratedWindow(onCloseRequest = ::exitApplication, title = "App") { MaterialTitleBar { state -> /* … */ } MyContent() } } ``` ## How it works macOS, Fluent and Yaru are **full design systems** (widgets, theme, icons) plus an optional desktop window module that wraps Nucleus `DecoratedWindow` / Tao. Jewel and Material in this repo are **window chrome adapters** only — you still pick the widget library yourself. Where the theme goes depends on the pack. Fluent, Jewel and Material derive the window chrome from the **ambient** theme, so wrap the window call — `FluentTheme { FluentDecoratedWindow { … } }` — and they re-provide the theme inside the content themselves. `JewelDecoratedWindow` reads `JewelTheme.globalColors` and throws without a provider. The macOS 26 and Yaru wrappers take their theme **inside** the window content instead. For full-window layouts and custom chrome without a design-system pack, use [`WindowScaffold`](/docs/tao/window-scaffold) on Tao. ## What's next - [macOS 26 toolkit](/docs/window/toolkit-macos) - [Fluent toolkit](/docs/window/toolkit-fluent) - [Yaru toolkit](/docs/window/toolkit-yaru) - [Jewel for tooling](/docs/window/toolkit-jewel) - [Material 2 / 3](/docs/window/toolkit-material) - [Window scaffold](/docs/tao/window-scaffold) --- # Context menus URL: https://nucleusframework.dev/en/docs/window/context-menu Replace Compose-drawn context menus with the OS-looking menu — NSMenu on macOS, a Fluent flyout on Windows, and Adwaita or Breeze on Linux. By default, Compose Desktop draws its own context menus, which do not match the desktop they run on. Nucleus can replace them with the platform menu: a real `NSMenu` on macOS, and a Compose flyout styled as Fluent on Windows, Adwaita on GTK desktops, or Breeze on Qt desktops. This page covers the opt-in, the item types, and how items reach the menu. ## Turn on the native menu Set `nativeContextMenu` on the window: ```kotlin import dev.nucleusframework.application.DecoratedWindow import dev.nucleusframework.application.nucleusApplication fun main() = nucleusApplication { DecoratedWindow( onCloseRequest = ::exitApplication, title = "MyApp", nativeContextMenu = true, ) { // Text fields now show the OS-looking Cut/Copy/Paste menu } } ``` This covers `ContextMenuArea`, the text-field Cut/Copy/Paste menu, and [spellcheck suggestions](/docs/os/spell-check). The parameter is also available on `HostedWindow`. The native menu is a [Tao backend](/docs/tao) feature. On the AWT backends the parameter is accepted and ignored, so the same code keeps Compose menus there. You do not choose a style. The menu follows the platform, and on Linux it follows the session's toolkit — Breeze on Qt desktops, Adwaita everywhere else. ## Add your own items Build items with `NucleusContextMenuItem`, which extends Compose's `ContextMenuItem` with an icon and a shortcut label: ```kotlin import androidx.compose.foundation.ContextMenuArea import androidx.compose.foundation.ContextMenuItem import dev.nucleusframework.application.contextmenu.ContextMenuIcon import dev.nucleusframework.application.contextmenu.NucleusContextMenuDivider import dev.nucleusframework.application.contextmenu.NucleusContextMenuItem ContextMenuArea( items = { listOf( NucleusContextMenuItem( label = "Reveal in Finder", icon = ContextMenuIcon.Folder, onClick = { revealInFileManager(path) }, ), NucleusContextMenuDivider, NucleusContextMenuItem( label = "Delete", icon = ContextMenuIcon.Delete, shortcut = "Del", onClick = { delete(path) }, ), ) }, ) { FileRow(path) } ``` `NucleusContextMenuDivider` is the separator, and `NucleusContextMenuSubmenu` nests a group. Its `items` lambda is evaluated when the menu opens, so a submenu can list something that changes: ```kotlin import dev.nucleusframework.application.contextmenu.NucleusContextMenuSubmenu NucleusContextMenuSubmenu(label = "Open recent") { recentFiles.map { fileItem(it) } } ``` ## Show keyboard shortcuts An item with a stock icon gets the platform shortcut label for free — `⌘C` on macOS, `Ctrl+C` elsewhere: ```kotlin NucleusContextMenuItem(label = "Copy", icon = ContextMenuIcon.Copy, onClick = ::copy) ``` Pass `shortcut` to override the label, or `shortcut = ""` to suppress the stock one. `ContextMenuIcon.stockShortcut()` returns the label used for a given icon. Shortcut labels are drawn by the Windows and Linux flyouts. macOS `NSMenu` does not render this field yet. ## Detect the Linux toolkit The Linux menu style is picked from the session, which you can also read yourself — for example to match an unrelated widget to the desktop: ```kotlin import dev.nucleusframework.core.runtime.LinuxUiToolkit when (LinuxUiToolkit.Current) { LinuxUiToolkit.Qt -> // KDE Plasma, LXQt, Deepin, … LinuxUiToolkit.Gtk -> // GNOME, Xfce, Cinnamon, MATE, … } ``` `LinuxUiToolkit.Current` reads `XDG_CURRENT_DESKTOP`, `DESKTOP_SESSION`, and `XDG_SESSION_DESKTOP`, then falls back to `KDE_FULL_SESSION` and `QT_QPA_PLATFORMTHEME`. Unknown desktops and bare tiling window managers resolve to `Gtk`. ## How it works Turning on `nativeContextMenu` installs two Compose representations for the window subtree: `NativeContextMenuRepresentation` for `ContextMenuArea`, and `NativeTextContextMenu` for text fields. Both walk the `ContextMenuItem` list Compose hands them and convert it to a `ContextMenuEntry` tree through the `ContextMenuItemInterpreter` in scope. That tree is then rendered per platform. macOS builds a real `NSMenu` through [`menu-macos`](/docs/os/menu-macos) and pops it up; Windows and Linux draw a Compose flyout at OS density, so the menu keeps system scale even inside a subtree that overrides `LocalDensity`. Items arrive from independent contributors — the field's own Cut/Copy/Paste, your extras, the spellcheck suggestions — so the entry list is normalized before it is shown: leading, trailing, and consecutive separators are dropped, recursively into submenus. An empty list closes the menu instead of flashing an empty frame. ## API reference Package `dev.nucleusframework.application.contextmenu`, shipped in `nucleus-application`. ### Build items | Symbol | Description | |---|---| | `NucleusContextMenuItem(label, enabled, icon, shortcut, onClick)` | A `ContextMenuItem` with an optional icon and shortcut label. | | `NucleusContextMenuDivider` | Separator item. | | `NucleusContextMenuSubmenu(label) { items }` | Nested group; `items` is evaluated when the menu opens. | | `ContextMenuIcon` | `Cut`, `Copy`, `Paste`, `SelectAll`, `Delete`, `Folder`, `SfSymbol(name)`. | | `ContextMenuIcon.stockShortcut()` | Platform shortcut label for the editing icons, else `null`. | ### Control the menu | Symbol | Description | |---|---| | `NativeContextMenuProvider(enabled) { }` | Installs the native menu for a subtree, without going through the window parameter. | | `isNativeContextMenuSupported` | Whether this platform can show it. | | `LocalNativeContextMenu` | Whether the subtree currently uses the native menu. | | `LocalContextMenuDivider` | The separator the active renderer draws, or `null`. Read it when you assemble item lists. | | `NativeContextMenuRepresentation` / `NativeTextContextMenu` | The representations installed by the provider. | ### Interpret items | Symbol | Description | |---|---| | `ContextMenuEntry` | `Item(label, enabled, icon, onClick, shortcut)`, `Separator`, `Submenu(label, items)`. | | `ContextMenuItemInterpreter` | Maps a Compose `ContextMenuItem` to a `ContextMenuEntry`. | | `DefaultContextMenuItemInterpreter` | The default mapping. | | `LocalContextMenuItemInterpreter` | Override the mapping for a subtree. | ### macOS popup primitives Package `dev.nucleusframework.menu.macos`, shipped in `menu-macos`. | Symbol | Description | |---|---| | `NativePopupMenuItem` | `Entry(title, enabled, icon, onClick)`, `Separator`, `Submenu(title, items)`. | | `isNativePopupMenuAvailable` | Whether the native bridge loaded. | | `popUpNativeMenu(items)` | Shows an `NSMenu` at the cursor. | Call `popUpNativeMenu` off the Tao UI thread. AppKit runs a nested tracking loop while the menu is open, and inside a Tao event callback that loop starves the event loop — animations, `Dispatchers.Main`, and `delay` all stop until the menu closes. ## Notes - Icons render differently per platform: Breeze draws 16 dp vectors, Fluent uses Segoe Fluent Icons glyphs, and Adwaita follows the GTK convention of no icons in context menus. `ContextMenuIcon.SfSymbol` applies to macOS only. - The flyout closes when the owning window loses focus. Outside-click monitoring only sees this process, so focus is the one signal every backend reports — and it also covers Alt+Tab and clicks on the taskbar. - On macOS the menu needs the `menu-macos` native library; if it fails to load, `isNativeContextMenuSupported` is `false` and Compose menus are used. ## What's next - [Spell check](/docs/os/spell-check) — suggestions inside these menus. - [macOS menu bar](/docs/os/menu-macos) — the application menu, same native layer. - [DecoratedWindow on Tao](/docs/tao/decorated-window) — the window that hosts the menu. --- # macOS 26 design system URL: https://nucleusframework.dev/en/docs/window/toolkit-macos Compose Multiplatform macOS 26 UI with Liquid Glass materials and a Nucleus-backed decorated window. [compose-macos-26-ui](https://github.com/NucleusFramework/compose-macos-26-ui) is a Compose Multiplatform design system for the macOS 26 look: components, theme tokens, icons, and a desktop `MacosDecoratedWindow` on the Nucleus Tao backend. It targets Android, iOS, desktop JVM, and web (JS/Wasm). Versioned independently of Nucleus (current release **1.0.0**). Live gallery: [nucleusframework.github.io/compose-macos-26-ui](https://nucleusframework.github.io/compose-macos-26-ui/). ## Add the dependency ```kotlin title="build.gradle.kts" plugins { id("dev.nucleusframework") } dependencies { implementation("dev.nucleusframework:compose-macos-ui:1.0.0") // optional: // implementation("dev.nucleusframework:compose-macos-ui-icons-extended:1.0.0") // implementation("dev.nucleusframework:compose-macos-ui-markdown:1.0.0") } ``` `compose-macos-ui` already exposes `nucleus.nucleus-application` and `nucleus.decorated-window-tao` as `api` on the JVM target. ## Open a decorated window ```kotlin import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.macoscompose.theme.MacosTheme import dev.nucleusframework.macoscompose.window.MacosDecoratedWindow fun main() = nucleusApplication { MacosDecoratedWindow( onCloseRequest = ::exitApplication, title = "Mail", ) { MacosTheme(darkTheme = false) { App() } } } ``` Put `MacosTheme` **inside** the window content so native surfaces (traffic lights, glass, clear colour) follow the app theme. ## How it works On desktop, `MacosDecoratedWindow` wraps Nucleus `DecoratedWindow` (Tao): full-size content view, large corner radius via a hidden `NSToolbar`, traffic lights recentred against `titleBarHeight`, and native materials. Windows and Linux use an undecorated frame with platform controls drawn in the same band. ## What's next - [Native design systems](/docs/window/toolkits) - [Window scaffold](/docs/tao/window-scaffold) — full-window layouts without this pack - [Repo README](https://github.com/NucleusFramework/compose-macos-26-ui) --- # Fluent design system URL: https://nucleusframework.dev/en/docs/window/toolkit-fluent Windows 11 Fluent UI for Compose Multiplatform, with a Nucleus-backed FluentDecoratedWindow. [compose-fluent-ui](https://github.com/NucleusFramework/compose-fluent-ui) is a maintained fork of the Fluent Design library for Compose Multiplatform (desktop, Android, iOS, web). The `decorated-window-fluent` module adds a Nucleus/Tao client-side decorated window with native Mica / Acrylic on Windows. Versioned independently of Nucleus (current release **1.0.0**). Live gallery: [nucleusframework.github.io/compose-fluent-ui](https://nucleusframework.github.io/compose-fluent-ui/). ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework.composefluent:fluent:1.0.0") implementation("dev.nucleusframework.composefluent:fluent-icons-extended:1.0.0") // optional // Desktop: Nucleus/Tao window implementation("dev.nucleusframework.composefluent:decorated-window-fluent:1.0.0") } ``` ## Open a Fluent window ```kotlin import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.window.fluent.FluentDecoratedWindow import dev.nucleusframework.window.fluent.FluentTitleBar import io.github.composefluent.FluentTheme fun main() = nucleusApplication { FluentTheme { FluentDecoratedWindow(onCloseRequest = ::exitApplication, title = "Settings") { FluentTitleBar { /* search, breadcrumbs, … */ } App() } } } ``` Widgets live under `io.github.composefluent.component`; theme entry point is `FluentTheme()`. ## How it works `decorated-window-fluent` maps Fluent theme colours onto Nucleus chrome and wires `WindowsBackdrop` for real DWM Mica / Acrylic on Windows. The component library itself is KMP; only the window module is JVM/desktop. ## What's next - [Native design systems](/docs/window/toolkits) - [Windows backdrops on Tao](/docs/tao/window-scaffold#windows-11-backdrops) - [Repo README](https://github.com/NucleusFramework/compose-fluent-ui) --- # Yaru design system URL: https://nucleusframework.dev/en/docs/window/toolkit-yaru Ubuntu Yaru widgets and themes for Compose Multiplatform, with a Nucleus-backed YaruDecoratedWindow. [yaru-compose-ui](https://github.com/NucleusFramework/yaru-compose-ui) ports [yaru.dart](https://github.com/ubuntu/yaru.dart) to Compose Multiplatform: 50+ widgets, accent variants, light/dark/high-contrast themes, and a GNOME-style `YaruDecoratedWindow` on Nucleus Tao. Versioned independently of Nucleus (current release **1.0.1**). Live gallery: [nucleusframework.github.io/yaru-compose-ui](https://nucleusframework.github.io/yaru-compose-ui/). ## Add the dependency Design system only (no native windowing): ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework.yarucompose:yaru:1.0.1") } ``` Desktop window (pulls `yaru` + Nucleus Tao transitively): ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework.yarucompose:yaru-decorated-window:1.0.1") } ``` ## Open a Yaru window ```kotlin import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.yarucompose.themes.YaruTheme import dev.nucleusframework.yarucompose.themes.YaruVariant import dev.nucleusframework.yarucompose.window.YaruDecoratedWindow fun main() = nucleusApplication { YaruDecoratedWindow(onCloseRequest = ::exitApplication, title = "Settings") { YaruTheme(isDark = false, variant = YaruVariant.Orange) { App() } } } ``` `YaruTheme` must sit **inside** the window content so chrome tracks the resolved colour scheme and layout direction (including RTL). ## How it works `yaru` is design-only and talks to windowing through optional composition locals. `yaru-decorated-window` fulfils those contracts with Nucleus and ships the native binaries; leave it out if you draw into a plain Compose `Window` or a non-JVM target. ## What's next - [Native design systems](/docs/window/toolkits) - [Window scaffold](/docs/tao/window-scaffold) - [Repo README](https://github.com/NucleusFramework/yaru-compose-ui) --- # Jewel — IntelliJ Platform URL: https://nucleusframework.dev/en/docs/window/toolkit-jewel Style a DecoratedWindow with Jewel colors and metrics so the title bar and chrome match the IntelliJ Platform look. Jewel is JetBrains' design system for the IntelliJ Platform. The Nucleus Jewel toolkit styles `DecoratedWindow` with Jewel colors and metrics, so the title bar and window chrome match the IntelliJ look on macOS, Windows, and Linux. It runs on the Tao backend behind the same API (the AWT backend is also supported, but deprecated). ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") implementation("dev.nucleusframework:nucleus.decorated-window-jewel:") // A Jewel theme module — pick the one that matches your target IDE build. implementation("org.jetbrains.jewel:jewel-int-ui-standalone-243:") } ``` The toolkit depends on `decorated-window-core` and a backend module. Pair it with `-tao` for the Rust-native windowing backend; the `-jbr` / `-jni` AWT modules are deprecated. ## Create a Jewel-styled window Wrap the window content in a Jewel theme, then open a `JewelDecoratedWindow`: ```kotlin import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.darkmodedetector.isSystemInDarkMode import dev.nucleusframework.window.jewel.JewelDecoratedWindow import dev.nucleusframework.window.jewel.JewelTitleBar import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme fun main() = nucleusApplication { IntUiTheme(isDark = isSystemInDarkMode()) { JewelDecoratedWindow( onCloseRequest = ::exitApplication, title = "Tools", ) { JewelTitleBar { /* title bar content */ } Box(Modifier.fillMaxSize()) { // Jewel widgets go here } } } } ``` The window reads the current Jewel theme and derives its title bar and chrome from it, so switching `isDark` flips the whole window between the light and dark IntelliJ palettes. `JewelDecoratedWindow` reads `JewelTheme.globalColors`, so you must call it inside a Jewel theme such as `IntUiTheme`. Without one, the composable has no colors to derive its style from. ## How it works `JewelDecoratedWindow` is a Composable wrapper around `DecoratedWindow`. It reads `JewelTheme.globalColors`, derives a `DecoratedWindowStyle` and a `TitleBarStyle` from the active Jewel theme, and installs them through `NucleusDecoratedWindowTheme` before opening the window. Inside the window you compose regular Jewel widgets — `Text`, `DefaultButton`, `OutlinedButton`, lazy lists, and the rest. There are two overloads. The one on `NucleusApplicationScope` — used above inside `nucleusApplication { }` — is backend-agnostic and works on both the Tao and AWT backends from the same call site. The overload on Compose's `ApplicationScope` is AWT-backed (JBR/JNI) and is meant for a plain `application { }` entry point. `rememberJewelWindowStyle()` and `rememberJewelTitleBarStyle()` expose the derived styles directly. Use them when you build your own wrapper — for example, a single composable that switches between Jewel and another toolkit at runtime. ## API reference ### Open a window or dialog - `JewelDecoratedWindow(...)` — a Jewel-styled window. Available on `NucleusApplicationScope` (backend-agnostic) and on `ApplicationScope` (AWT-backed). The `NucleusApplicationScope` overload also takes `undecorated`, `popupFor`, `nativeContextMenu`, `hiddenFromDock`, `titleBarStyle`, and the Tao-only flags `transparent`, `clickThrough`, `visibleOnAllWorkspaces` and `forceX11`; the AWT overload does not. - `JewelDecoratedDialog(...)` — the modal dialog variant. A top-level function is AWT-backed; the `NucleusApplicationScope` overload is backend-agnostic. ### Add a title bar - `DecoratedWindowScope.JewelTitleBar(...)` — the title bar slot for a window. - `DecoratedDialogScope.JewelDialogTitleBar(...)` — the title bar slot for a dialog. ### Read the derived styles - `rememberJewelWindowStyle(): DecoratedWindowStyle` — the window chrome style derived from the active Jewel theme. - `rememberJewelTitleBarStyle(): TitleBarStyle` — the title bar style derived from the active Jewel theme. ## What's next - [Material toolkit](/docs/window/toolkit-material) — Material 2 and Material 3 styling for `DecoratedWindow`. - [DecoratedWindow](/docs/window) — the underlying window and title bar API. - [Backends](/docs/concepts/backends) — Tao and the deprecated AWT backend. --- # Material 2 & Material 3 URL: https://nucleusframework.dev/en/docs/window/toolkit-material Theme a Nucleus DecoratedWindow from a Compose MaterialTheme, using either the Material 2 or Material 3 adapter. The Material toolkits theme a Nucleus `DecoratedWindow` from a Compose `MaterialTheme`, so the title bar and window controls follow your app's color scheme on every backend. Nucleus ships two adapters: `decorated-window-material2` for `androidx.compose.material` and `decorated-window-material3` for `androidx.compose.material3`. ## Add the dependency Add the Nucleus runtime, a backend, and the Material adapter that matches the Compose Material version you use. Pair it with `decorated-window-tao` for the Tao backend. **Material 3** ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") implementation("dev.nucleusframework:nucleus.decorated-window-material3:") implementation(compose.material3) } ``` **Material 2** ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") implementation("dev.nucleusframework:nucleus.decorated-window-tao:") implementation("dev.nucleusframework:nucleus.decorated-window-material2:") implementation(compose.material) } ``` The Material 3 artifact is `nucleus.decorated-window-material3`, but its Kotlin package is `dev.nucleusframework.window.material` — with no `3`. Material 2 lives in `dev.nucleusframework.window.material2`. ## Create a Material window Wrap `MaterialDecoratedWindow` in a `MaterialTheme`. The window inherits the theme's colors, and `MaterialTitleBar` draws a title bar styled to match. **Material 3** ```kotlin import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.lightColorScheme import androidx.compose.ui.Modifier import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.window.material.MaterialDecoratedWindow import dev.nucleusframework.window.material.MaterialTitleBar fun main() = nucleusApplication { MaterialTheme(colorScheme = lightColorScheme()) { MaterialDecoratedWindow(onCloseRequest = ::exitApplication, title = "Material 3") { MaterialTitleBar { Text("Material 3") } Surface(Modifier.fillMaxSize()) { /* content */ } } } } ``` **Material 2** ```kotlin import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.lightColors import androidx.compose.ui.Modifier import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.window.material2.MaterialDecoratedWindow import dev.nucleusframework.window.material2.MaterialTitleBar fun main() = nucleusApplication { MaterialTheme(colors = lightColors()) { MaterialDecoratedWindow(onCloseRequest = ::exitApplication, title = "Material 2") { MaterialTitleBar { Text("Material 2") } Surface(Modifier.fillMaxSize()) { /* content */ } } } } ``` Swap `lightColorScheme()` / `lightColors()` for the dark variant to switch the whole window, chrome included. To follow the operating system, drive the color scheme from `isSystemInDarkMode()` in the `nucleus.darkmode-detector` artifact. ## How it works Inside `nucleusApplication { }`, `MaterialDecoratedWindow` reads the current `MaterialTheme`, derives a `DecoratedWindowStyle` and a `TitleBarStyle` from it, and wraps the window in `NucleusDecoratedWindowTheme`. The title bar background, content color, and window-control button colors are taken from your `ColorScheme` (Material 3) or `Colors` (Material 2), so the chrome updates whenever the theme switches to a dark palette. Material 3 exposes the color mapping as public helpers you can call yourself: ```kotlin val windowStyle = rememberMaterialWindowStyle(colorScheme) val titleBarStyle = rememberMaterialTitleBarStyle(colorScheme) ``` Pass a `TitleBarStyle` to the `titleBarStyle` parameter of `MaterialDecoratedWindow` to override the derived title bar; the window style is always resolved from the active `ColorScheme`. Material 2 derives the same styles internally, but those helpers are `internal` and not part of its public API. ## API reference ### Material 3 - `ApplicationScope.MaterialDecoratedWindow(...)` and `NucleusApplicationScope.MaterialDecoratedWindow(...)` — the `NucleusApplicationScope` overload also takes `nativePopupLayers`, `nativeContextMenu`, `hiddenFromDock`, `titleBarStyle`, `undecorated`, and the Tao-only flags `transparent`, `clickThrough`, `visibleOnAllWorkspaces` and `forceX11` (Material 2 has the same set minus `nativePopupLayers`) - `ApplicationScope.MaterialDecoratedDialog(...)` and `NucleusApplicationScope.MaterialDecoratedDialog(...)` - `DecoratedWindowScope.MaterialTitleBar(...)` - `DecoratedDialogScope.MaterialDialogTitleBar(...)` - `rememberMaterialWindowStyle(colorScheme: ColorScheme): DecoratedWindowStyle` - `rememberMaterialTitleBarStyle(colorScheme: ColorScheme): TitleBarStyle` ### Material 2 - `MaterialDecoratedWindow(...)` and `NucleusApplicationScope.MaterialDecoratedWindow(...)` - `MaterialDecoratedDialog(...)` - `DecoratedWindowScope.MaterialTitleBar(...)` - `DecoratedDialogScope.MaterialDialogTitleBar(...)` The Material adapters run on every backend. Pair them with `decorated-window-tao` for the Tao backend, or with the deprecated `-jbr` / `-jni` AWT modules. ## What's next - [Window and toolkits](/docs/window) — the `DecoratedWindow` API the Material adapters wrap. - [Choosing a toolkit](/docs/window/toolkits) — compare Material with the native looks. - [Jewel toolkit](/docs/window/toolkit-jewel) — IDE-style chrome built on Jewel. --- # PDF reader URL: https://nucleusframework.dev/en/docs/pdf Render PDFs, extract text, and select or copy content from Compose, with one API across Android, iOS, web, and desktop. The PDF reader is a Kotlin Multiplatform library that renders PDF pages, extracts text, and drives selection, search, links, and thumbnails from Compose. The public API is the same on every target: you write against `PdfReaderState` and `PdfPage` once, and the library binds to the native PDF stack of each platform underneath. The PDF reader ships from its own repository with its own release cycle, like the [system tray](/docs/os/system-tray). Its version number is **independent of Nucleus** — pin it separately. The artifact is `dev.nucleusframework:pdfium`, published to Maven Central. ## One API instead of one per platform Each platform has its own PDF engine, with its own API, threading model, and coordinate conventions: | Platform | Native PDF stack | | -------- | ---------------- | | Android | `PdfRenderer` (`android.graphics.pdf`) | | iOS | PDFKit (`PDFView`, `PDFDocument`) | | Web | PDF.js | | Desktop | PDFBox and other JVM libraries | Without a shared layer, a multiplatform app integrates and maintains four of these. The PDF reader replaces them with a single Compose surface backed by [PDFium](https://pdfium.googlesource.com/pdfium/) on all four, so rendering, text extraction, and selection behave identically everywhere. ## Add the dependency ```kotlin title="build.gradle.kts" kotlin { sourceSets { commonMain.dependencies { implementation("dev.nucleusframework:pdfium:") } } } ``` The `:pdfium` module uses a JVM toolchain of 17. PDFium binaries are fetched at build time from [bblanchon/pdfium-binaries](https://github.com/bblanchon/pdfium-binaries); no manual native setup is required. ## Show a PDF Hoist a `PdfReaderState`, open the document bytes, and drop `PdfReader` into your layout: ```kotlin import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import dev.nucleusframework.pdfium.PdfReader import dev.nucleusframework.pdfium.rememberPdfReaderState @Composable fun HelloPdf(bytes: ByteArray) { val reader = rememberPdfReaderState() LaunchedEffect(bytes) { reader.open(bytes) } PdfReader(state = reader, modifier = Modifier.fillMaxSize()) } ``` `PdfReader` stacks every page in a `LazyColumn`. `rememberPdfReaderState` disposes the native handles automatically when it leaves composition. ## Supported targets | Target | Backend | | ------- | ----------------------------------------------- | | JVM | JNI + Skia (Skiko) | | Android | JNI (NDK `AndroidBitmap_*`) | | iOS | Kotlin/Native cinterop + Skia (Skiko) | | Web | `pdfium.wasm` in a Web Worker + Skiko | JVM covers linux-x64/arm64, macos-x64/arm64, and win-x64/arm64. Android covers arm64-v8a, armeabi-v7a, x86, and x86_64. Web covers Kotlin/WasmJS and Kotlin/JS (IR). ## How it works `commonMain` holds the whole public API — `PdfReaderState`, `PdfPage`, `PdfThumbnail`, and `PdfReader` — over an `expect class PdfDocument`. Each source set provides the `actual`: JNI glue on JVM and Android, a Kotlin/Native cinterop on iOS, and a `pdfium.wasm` worker on the web. Rendering uses a zero-copy path where the platform allows it. On JVM and iOS, PDFium writes pixels straight into a Skia bitmap's memory; on Android it locks the `android.graphics.Bitmap`; on the web the worker's pixel `ArrayBuffer` is written into Skia's wasm heap without an intermediate Kotlin `ByteArray`. PDFium is single-threaded per document — it relies on FreeType's non-thread-safe `FT_Library` — so each `PdfDocument` runs on its own single-threaded dispatcher. Rendering is serialised inside a document. ## What the library covers - Compose composables: drop `PdfReader`, `PdfPage`, or `PdfThumbnail` into any Compose UI. - Progressive rendering (preview to full) with a debounced size flow. - A two-tier LRU cache for reader bitmaps and thumbnails, with off-screen prefetch. - Text extraction: per-page UTF-8 text, line-level rectangles, and per-character boxes. - A selectable-text overlay driven by PDFium's per-character boxes. - Clickable links: URI and internal GoTo annotations, plus URLs and e-mail addresses detected in the page text. - Password-protected documents through `PdfError.PasswordRequired`. PDFium is dual-licensed BSD-3-Clause / Apache-2.0, and bblanchon's binaries carry that license forward. If you ship an app built on this library, include the upstream PDFium notices. ## What's next - [Getting started](/docs/pdf/getting-started) — load bytes, handle loading state, enable selection, links, thumbnails, and zoom. - [API reference](/docs/pdf/api-reference) — every public type, its parameters, and defaults. - [WebView](/docs/webview) — another Nucleus component shipped from its own repository. - [System tray](/docs/os/system-tray) — tray icons and menus from a separate repository. --- # Getting started URL: https://nucleusframework.dev/en/docs/pdf/getting-started Load PDF bytes, react to loading state, and enable text selection, links, thumbnails, and zoom. In this tutorial, you'll open a PDF from a file picker, react to its loading state, and turn on text selection, links, a thumbnail sidebar, and zoom. Every snippet drops into a Compose Multiplatform `commonMain` source set. ## Before you start Add the dependency described in the [PDF reader overview](/docs/pdf): ```kotlin title="build.gradle.kts" commonMain.dependencies { implementation("dev.nucleusframework:pdfium:") } ``` ## Load the PDF bytes `PdfReaderState.open` takes a `ByteArray` — how you obtain it is up to you. To pick a file with a native dialog, use [FileKit](/docs/ecosystem/file-dialog): ```kotlin @Composable fun PdfPicker() { val reader = rememberPdfReaderState() val scope = rememberCoroutineScope() val picker = rememberFilePickerLauncher( type = FileKitType.File(extensions = listOf("pdf")), ) { file -> if (file != null) scope.launch { reader.open(file.readBytes()) } } Column(Modifier.fillMaxSize()) { Button(onClick = { picker.launch() }) { Text("Open PDF…") } if (reader.pageCount > 0) { PdfReader(state = reader, modifier = Modifier.fillMaxSize()) } } } ``` To load from a URL, pass the bytes from any HTTP client: ```kotlin val client = remember { HttpClient() } LaunchedEffect(url) { reader.open(client.get(url).readRawBytes()) } ``` ## React to loading state and errors `PdfReaderState` exposes snapshot state you can read in any composable — `isLoading`, `error`, `metadata`, and `pageCount`: ```kotlin Box(Modifier.fillMaxSize()) { when { reader.isLoading -> CircularProgressIndicator(Modifier.align(Alignment.Center)) reader.error is PdfError.PasswordRequired -> PasswordPrompt { password -> scope.launch { reader.open(bytes, password) } } reader.error is PdfError.InvalidFormat -> Text("Not a valid PDF") reader.error != null -> Text("Couldn't open: ${reader.error?.message}") reader.pageCount > 0 -> { Column { Text(reader.metadata.title ?: "Untitled") PdfReader(state = reader, modifier = Modifier.weight(1f)) } } } } ``` ## Enable text selection Set `selectableText = true` on `PdfPage`. Drag on desktop, long-press on mobile, and copy with Ctrl/Cmd+C: ```kotlin PdfPage( state = reader, pageIndex = pageIndex, modifier = Modifier.fillMaxWidth(), selectableText = true, ) ``` Hit-testing uses PDFium's per-character boxes (`FPDFText_GetCharBox`), so selection tracks the rendered glyphs rather than Compose's own font metrics. ## Handle links Links are enabled by default on `PdfPage` and `PdfReader`. Link annotations and URLs or e-mail addresses detected in the page text become clickable regions. External URIs open through `LocalUriHandler`; inside `PdfReader`, internal GoTo links scroll to the destination page. Intercept clicks with `onLinkClick` — return `true` to consume the click and skip the default handling: ```kotlin PdfReader( state = reader, onLinkClick = { link -> if (link.uri?.startsWith("mailto:") == true) { openCustomComposer(link.uri) true } else { false } }, ) ``` ## Add a thumbnail sidebar `PdfThumbnail` renders at `RenderQuality.PREVIEW` and keeps its own LRU, so scrolling a long strip never evicts the reader's full-quality bitmaps: ```kotlin Row(Modifier.fillMaxSize()) { LazyColumn(Modifier.width(160.dp).fillMaxHeight()) { items(reader.pageCount) { i -> PdfThumbnail(state = reader, pageIndex = i, modifier = Modifier.clickable { /* jumpToPage(i) */ }) } } PdfReader(state = reader, modifier = Modifier.weight(1f).fillMaxHeight()) } ``` ## Zoom and fit-to-width `PdfReaderState.renderScale` is a plain `Float` that every `PdfPage` observes. `1.0` is fit-to-width; change it to re-render the visible pages at the new size: ```kotlin var scale by remember { mutableStateOf(1f) } LaunchedEffect(scale) { reader.renderScale = scale } Slider(value = scale, onValueChange = { scale = it }, valueRange = 0.5f..3f) ``` ## Extract text programmatically `pageText` returns the Unicode of a single page; `pageTextLayout` returns line-level rectangles for search and highlighting: ```kotlin scope.launch { val text = reader.pageText(pageIndex = 0) println(text) } ``` The library's sample app wires a picker, sidebar, zoom, and selection into a full reader screen. See the repository's `:example` module for a complete reference. ## What's next - [API reference](/docs/pdf/api-reference) — every public type, its parameters, and defaults. - [PDF reader overview](/docs/pdf) — supported targets and how the render path works. - [Native file dialogs](/docs/ecosystem/file-dialog) — the FileKit picker used above. --- # API reference URL: https://nucleusframework.dev/en/docs/pdf/api-reference PdfReaderState, PdfPage, PdfThumbnail, PdfReader, and the supporting data types. The public API lives under `dev.nucleusframework.pdfium`. Coordinates in text-layout results are in PDF page points (1 pt = 1/72 inch), with the origin at the bottom-left of the page. ## PdfReaderState The state holder tied to a single document. Hoist it with `rememberPdfReaderState()`. ```kotlin @Stable class PdfReaderState { // Snapshot state val pageCount: Int // 0 until a document is open val isLoading: Boolean // true during open() val error: PdfError? // last open() error, if any val metadata: PdfMetadata var renderScale: Float // 1.0 = fit-to-width // Intents suspend fun open(bytes: ByteArray, password: String? = null) suspend fun pageSize(pageIndex: Int): PageSize? suspend fun pageText(pageIndex: Int): String suspend fun pageTextLayout(pageIndex: Int): PageTextLayout? // Render ahead-of-display; best-effort, populates the cache fun prefetch(pageIndex: Int, widthPx: Int, quality: RenderQuality = RenderQuality.FULL) // Release native handles + cached bitmaps (called by rememberPdfReaderState) fun dispose() companion object { const val DEFAULT_CACHE_BYTES: Long = 64L * 1024 * 1024 const val DEFAULT_THUMBNAIL_CACHE_BYTES: Long = 12L * 1024 * 1024 } } @Composable fun rememberPdfReaderState( cacheBytes: Long = PdfReaderState.DEFAULT_CACHE_BYTES, thumbnailCacheBytes: Long = PdfReaderState.DEFAULT_THUMBNAIL_CACHE_BYTES, ): PdfReaderState ``` Bitmaps are keyed by `(pageIndex, quantized_width)`. Both cache budgets are tunable on `rememberPdfReaderState(...)`. ## PdfPage Renders a single page. Handles progressive rendering (low-res preview to full quality on settle) and debounces size changes internally. ```kotlin @Composable fun PdfPage( state: PdfReaderState, pageIndex: Int, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit, background: Color = Color.White, selectableText: Boolean = false, linksEnabled: Boolean = true, onLinkClick: ((PdfLink) -> Boolean)? = null, ) ``` - `modifier` controls the layout width; the composable derives the aspect ratio from the PDF page and sets its own height. - `selectableText = true` enables the pointer-driven selection overlay. - A standalone `PdfPage` has no list to scroll, so internal GoTo links are only actionable through `onLinkClick`. ## PdfThumbnail A low-resolution preview of a single page. Renders at `RenderQuality.PREVIEW`, shares the `PdfReaderState` cache, and sizes itself to the modifier-provided width. ```kotlin @Composable fun PdfThumbnail( state: PdfReaderState, pageIndex: Int, modifier: Modifier = Modifier, background: Color = Color.White, ) ``` ## PdfReader A convenience composable — a vertical `LazyColumn` that stacks every page of the document. ```kotlin @Composable fun PdfReader( state: PdfReaderState, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(12.dp), pageSpacing: Dp = 16.dp, ) ``` ## RenderQuality ```kotlin enum class RenderQuality { // No annotations, no LCD text. Used for thumbnails and progressive previews. PREVIEW, // Annotations on, no LCD text. Balanced default for on-screen viewing. FULL, } ``` ## PageSize, PdfMetadata, PdfError ```kotlin data class PageSize(val widthPoints: Float, val heightPoints: Float) { val aspectRatio: Float // widthPoints / heightPoints, or 1f for degenerate pages } data class PdfMetadata( val title: String? = null, val author: String? = null, val subject: String? = null, val keywords: String? = null, val creator: String? = null, val producer: String? = null, ) sealed class PdfError(open val message: String, open val cause: Throwable? = null) { data class InvalidFormat(...) : PdfError(...) data class PasswordRequired(...) : PdfError(...) data class NativeFailure(...) : PdfError(...) data class Io(...) : PdfError(...) } ``` ## PageTextLayout Returned by `PdfReaderState.pageTextLayout(...)` for building custom text overlays and highlighting tools. ```kotlin @Immutable class PageTextLayout { val pageIndex: Int val pageSize: PageSize val rectCount: Int val charCount: Int // Rect-level (line-level runs from FPDFText_GetRect) fun left(i: Int): Float // in PDF points, origin bottom-left fun bottom(i: Int): Float fun right(i: Int): Float fun top(i: Int): Float fun text(i: Int): String // UTF-8 Unicode // Char-level (FPDFText_GetCharBox / FPDFText_GetUnicode) fun codepoint(i: Int): Int fun charLeft(i: Int): Float fun charBottom(i: Int): Float fun charRight(i: Int): Float fun charTop(i: Int): Float } ``` To map a rectangle to a rendered bitmap of pixel dimensions `W × H`: ``` scaleX = W / pageSize.widthPoints scaleY = H / pageSize.heightPoints screenX = left × scaleX screenY = H - top × scaleY // flip Y (PDF is bottom-up) screenW = (right - left) × scaleX screenH = (top - bottom) × scaleY ``` ## What's next - [Getting started](/docs/pdf/getting-started) — the same API used across a full reader screen. - [PDF reader overview](/docs/pdf) — supported targets and the render path. --- # WebView URL: https://nucleusframework.dev/en/docs/webview Embed a native web engine in Compose with one API across Android, iOS, web, and desktop. The WebView library embeds each platform's native web engine in Compose. You write against `WebView`, `WebViewState`, and `WebViewNavigator` once; the library binds to Android WebView, WKWebView, WebView2, WebKit2GTK, or an iframe underneath. The public API and the Android/iOS implementations follow [compose-webview-multiplatform](https://github.com/KevinnZou/compose-webview-multiplatform). ComposeNativeWebView adds a WasmJs target and a desktop path on the [Tao `NativeView`](/docs/tao/native-views) stack. WebView ships from [`NucleusFramework/ComposeNativeWebview`](https://github.com/NucleusFramework/ComposeNativeWebview) with its own release cycle, like the [system tray](/docs/os/system-tray) and the [PDF reader](/docs/pdf). Its version number is **independent of Nucleus** — pin it separately. The artifact is `dev.nucleusframework:composewebview`, published to Maven Central. ## One API instead of one per platform | Platform | Native web engine | | -------- | ----------------- | | Android | `android.webkit.WebView` | | iOS | WKWebView | | WasmJs | `HTMLIFrameElement` | | Desktop (Linux) | WebKit2GTK via Tao `NativeView` | | Desktop (macOS) | WKWebView via Tao `NativeView` | | Desktop (Windows) | WebView2 CompositionController via Tao `NativeView` | Without a shared layer, a multiplatform app integrates and maintains each engine separately. ComposeNativeWebView replaces that with a single Compose surface and the same navigation, cookie, and JavaScript model on every target. ## Add the dependency ```kotlin title="build.gradle.kts" kotlin { sourceSets { commonMain.dependencies { implementation("dev.nucleusframework:composewebview:") } } } ``` The same artifact covers Android, iOS, Desktop (JVM), and WasmJs. ## Show a page Hoist a `WebViewState` and drop `WebView` into your layout: ```kotlin import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.nucleusframework.webview.web.WebView import dev.nucleusframework.webview.web.rememberWebViewState @Composable fun HelloWeb() { val state = rememberWebViewState("https://example.com") WebView(state = state, modifier = Modifier.fillMaxSize()) { // Optional Compose overlay on top of the native surface } } ``` The trailing `content` lambda draws Compose UI over the embedded engine — the same role as the content slot on Tao [`NativeView`](/docs/tao/native-views). ## Supported targets | Target | Backend | | ------- | ------- | | Android | `android.webkit.WebView` | | iOS | WKWebView | | WasmJs | IFrame | | JVM desktop | Tao + platform native library | Desktop requires a Nucleus Tao window (`nucleusApplication` and `decorated-window-tao`). Windows also needs the [WebView2 Runtime](https://developer.microsoft.com/microsoft-edge/webview2/) (or Edge). Native libraries ship inside the artifact under `nucleus/native/…`. ## What the library covers - Content loading: URL (with headers), inline HTML, and HTML from app resources. - Navigation: back, forward, reload, stop, with `canGoBack` / `canGoForward` as Compose state. - Observable state: `isLoading`, `loadingState`, `lastLoadedUrl`, `pageTitle`, and per-request errors. - A unified cookie manager across platforms. - `evaluateJavaScript` and a callback-based JS ↔ Kotlin bridge (`window.kmpJsBridge`). - A `RequestInterceptor` for navigator-initiated main-frame navigations. ## Limitations - `RequestInterceptor` does not intercept sub-resources (images, XHR, scripts). - **Desktop** needs the Tao backend; the library does not embed a browser on the AWT path. - **WasmJs**: history back/forward is unavailable in the iframe; CORS must allow embedding; JavaScript runs only on the same origin; cookies apply only when the iframe destination matches the parent origin. ## What's next - [Getting started](/docs/webview/getting-started) — load URLs and HTML, navigate, evaluate JS, intercept requests. - [API reference](/docs/webview/api-reference) — every public type, its parameters, and defaults. - [PDF reader](/docs/pdf) — another Nucleus component shipped from its own repository. - [Native views on Tao](/docs/tao/native-views) — the desktop embedding primitive used under the hood. --- # Getting started URL: https://nucleusframework.dev/en/docs/webview/getting-started Load URLs and HTML, navigate the history stack, evaluate JavaScript, and intercept requests from Compose. In this tutorial, you'll open a URL in a multiplatform WebView, react to loading state, navigate the history stack, evaluate JavaScript, and intercept navigations. Every snippet drops into a Compose Multiplatform `commonMain` source set. ## Before you start Add the dependency described in the [WebView overview](/docs/webview): ```kotlin title="build.gradle.kts" commonMain.dependencies { implementation("dev.nucleusframework:composewebview:") } ``` On **desktop**, open the window with Nucleus Tao (`nucleusApplication` and `decorated-window-tao`). The WebView embeds through [`NativeView`](/docs/tao/native-views) and does not run on the AWT backend. ## Load a URL Hoist a `WebViewState` with `rememberWebViewState` and place `WebView` in the layout: ```kotlin import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.nucleusframework.webview.web.WebView import dev.nucleusframework.webview.web.rememberWebViewState @Composable fun Browser(url: String) { val state = rememberWebViewState(url) { customUserAgentString = "MyApp/1.0" } WebView(state = state, modifier = Modifier.fillMaxSize()) } ``` The trailing lambda on `rememberWebViewState` is a `WebSettings` receiver — set user agent, zoom, JavaScript, and platform-specific options there. ## Load HTML For inline HTML, use `rememberWebViewStateWithHTMLData`. For a file packaged with the app, use `rememberWebViewStateWithHTMLFile`: ```kotlin val htmlState = rememberWebViewStateWithHTMLData( data = """

Hello

""".trimIndent(), baseUrl = null, ) val fileState = rememberWebViewStateWithHTMLFile( fileName = "index.html", readType = WebViewFileReadType.ASSET_RESOURCES, ) ``` `WebViewFileReadType.ASSET_RESOURCES` reads platform asset roots; `COMPOSE_RESOURCE_FILES` reads Compose Multiplatform resources. ## React to loading state `WebViewState` exposes snapshot state you can read in any composable — `isLoading`, `loadingState`, `lastLoadedUrl`, `pageTitle`, and `errorsForCurrentRequest`: ```kotlin Box(Modifier.fillMaxSize()) { WebView(state = state, modifier = Modifier.fillMaxSize()) if (state.isLoading) { LinearProgressIndicator( progress = { (state.loadingState as? LoadingState.Loading)?.progress ?: 0f }, modifier = Modifier.fillMaxWidth().align(Alignment.TopCenter), ) } state.pageTitle?.let { title -> Text(title, Modifier.align(Alignment.BottomCenter)) } } ``` `LoadingState` is a sealed class: `Initializing`, `Loading(progress)`, or `Finished`. ## Navigate with a navigator Hoist a `WebViewNavigator` when you need toolbar actions or programmatic loads: ```kotlin val state = rememberWebViewState("https://example.com") val navigator = rememberWebViewNavigator() Column(Modifier.fillMaxSize()) { Row { IconButton( onClick = { navigator.navigateBack() }, enabled = navigator.canGoBack, ) { Icon(Icons.Default.ArrowBack, contentDescription = "Back") } IconButton( onClick = { navigator.navigateForward() }, enabled = navigator.canGoForward, ) { Icon(Icons.Default.ArrowForward, contentDescription = "Forward") } IconButton(onClick = { navigator.reload() }) { Icon(Icons.Default.Refresh, contentDescription = "Reload") } IconButton(onClick = { navigator.loadUrl("https://kotlinlang.org") }) { Text("Kotlin") } } WebView(state = state, navigator = navigator, modifier = Modifier.weight(1f)) } ``` Navigator commands include `loadUrl`, `loadHtml`, `loadHtmlFile`, `evaluateJavaScript`, `navigateBack`, `navigateForward`, `reload`, and `stopLoading`. ## Evaluate JavaScript ```kotlin navigator.evaluateJavaScript("document.title") { title -> println("Page title: $title") } ``` ## Bridge JavaScript to Kotlin Pass a `WebViewJsBridge` to `WebView`. Handlers register under a method name; the page calls `window.kmpJsBridge.callNative(...)` after load: ```kotlin val navigator = rememberWebViewNavigator() val bridge = rememberWebViewJsBridge(navigator) LaunchedEffect(bridge) { bridge.register(object : IJsMessageHandler { override fun methodName() = "echo" override fun handle( message: JsMessage, navigator: WebViewNavigator?, callback: (String) -> Unit, ) { callback(message.params) } }) } WebView( state = state, navigator = navigator, webViewJsBridge = bridge, modifier = Modifier.fillMaxSize(), ) ``` From the page: ```js window.kmpJsBridge.callNative("echo", JSON.stringify({ hello: "world" }), function (result) { console.log(result); }); ``` ## Intercept navigations Attach a `RequestInterceptor` when creating the navigator. It runs only for **navigator-initiated** main-frame loads — not for sub-resources: ```kotlin val navigator = rememberWebViewNavigator( requestInterceptor = object : RequestInterceptor { override fun onInterceptUrlRequest( request: WebRequest, navigator: WebViewNavigator, ): WebRequestInterceptResult { if (request.url.contains("blocked.example")) { return WebRequestInterceptResult.Reject } return WebRequestInterceptResult.Allow } }, ) ``` Return `Allow`, `Reject`, or `Modify(request)` to rewrite the URL or headers. ## Manage cookies ```kotlin scope.launch { state.cookieManager.setCookie( url = "https://example.com", cookie = Cookie(name = "session", value = "abc", domain = "example.com"), ) val cookies = state.cookieManager.getCookies("https://example.com") state.cookieManager.removeCookies("https://example.com") state.cookieManager.removeAllCookies() } ``` ## Desktop notes - Linux needs WebKit2GTK available on the system (`libwebkit2gtk`). - Windows needs the WebView2 Runtime or a recent Edge install. - macOS uses the system WKWebView; no extra runtime package is required. - Open the host window on Tao. See [DecoratedWindow on Tao](/docs/tao/decorated-window). The repository's e2e hosts (`e2e-desktop`, `e2e-android`, `e2e-wasmJs`, `iosApp`) run the same visual suite against a real WebView on each platform. Use them as a complete reference for wiring. ## What's next - [API reference](/docs/webview/api-reference) — every public type, its parameters, and defaults. - [WebView overview](/docs/webview) — supported targets and platform backends. - [Native views on Tao](/docs/tao/native-views) — how desktop embeds the native surface. --- # API reference URL: https://nucleusframework.dev/en/docs/webview/api-reference WebView, WebViewState, WebViewNavigator, settings, cookies, JS bridge, and request interception. The public API lives under `dev.nucleusframework.webview`. Platform packages re-export the same types from `commonMain`. ## WebView The multiplatform composable. Draws the native engine and an optional Compose overlay. ```kotlin @Composable fun WebView( state: WebViewState, modifier: Modifier = Modifier, navigator: WebViewNavigator = rememberWebViewNavigator(), webViewJsBridge: WebViewJsBridge? = null, onCreated: (NativeWebView) -> Unit = {}, onDispose: (NativeWebView) -> Unit = {}, content: @Composable () -> Unit = {}, ) ``` - `content` is Compose UI drawn **over** the native surface (Tao `NativeView` content slot on desktop; a layered sibling elsewhere). - `onCreated` / `onDispose` receive the platform `NativeWebView` handle when the engine is attached or released. ## WebViewState State holder for one embedded engine. Create it with the `rememberWebViewState*` helpers. ```kotlin @Stable class WebViewState(webContent: WebContent) { var lastLoadedUrl: String? // last committed URL var content: WebContent // drives loads when reassigned var loadingState: LoadingState // Initializing | Loading | Finished val isLoading: Boolean // true until Finished var pageTitle: String? val errorsForCurrentRequest: SnapshotStateList val webSettings: WebSettings val cookieManager: CookieManager var webView: IWebView? // set after the platform view attaches } ``` ```kotlin @Composable fun rememberWebViewState( url: String, additionalHttpHeaders: Map = emptyMap(), extraSettings: WebSettings.() -> Unit = {}, ): WebViewState @Composable fun rememberWebViewStateWithHTMLData( data: String, baseUrl: String? = null, encoding: String = "utf-8", mimeType: String? = null, historyUrl: String? = null, ): WebViewState @Composable fun rememberWebViewStateWithHTMLFile( fileName: String, readType: WebViewFileReadType, ): WebViewState ``` ## WebContent ```kotlin sealed class WebContent { data class Url( val url: String, val additionalHttpHeaders: Map = emptyMap(), ) : WebContent() data class Data( val data: String, val baseUrl: String? = null, val encoding: String = "utf-8", val mimeType: String? = null, val historyUrl: String? = null, ) : WebContent() data class File( val fileName: String, val readType: WebViewFileReadType, ) : WebContent() data object NavigatorOnly : WebContent() } enum class WebViewFileReadType { ASSET_RESOURCES, COMPOSE_RESOURCE_FILES, } ``` ## LoadingState and WebViewError ```kotlin sealed class LoadingState { data object Initializing : LoadingState() data class Loading(val progress: Float) : LoadingState() data object Finished : LoadingState() } @Immutable data class WebViewError( val code: Int, val description: String, val isFromMainFrame: Boolean, ) ``` ## WebViewNavigator Programmatic navigation and script evaluation. Hoist with `rememberWebViewNavigator`. ```kotlin @Stable class WebViewNavigator( val coroutineScope: CoroutineScope, val requestInterceptor: RequestInterceptor? = null, ) { var canGoBack: Boolean var canGoForward: Boolean fun loadUrl(url: String, additionalHttpHeaders: Map = emptyMap()) fun loadHtml( html: String, baseUrl: String? = null, mimeType: String? = null, encoding: String? = "utf-8", historyUrl: String? = null, ) fun loadHtmlFile( fileName: String, readType: WebViewFileReadType = WebViewFileReadType.ASSET_RESOURCES, ) fun evaluateJavaScript(script: String, callback: ((String) -> Unit)? = null) fun navigateBack() fun navigateForward() fun reload() fun stopLoading() } @Composable fun rememberWebViewNavigator( coroutineScope: CoroutineScope = rememberCoroutineScope(), requestInterceptor: RequestInterceptor? = null, ): WebViewNavigator ``` Bare HTTP(S) host URLs without a path gain a trailing slash before load (browser-style normalization). ## WebSettings ```kotlin @Stable class WebSettings { var isJavaScriptEnabled: Boolean // default true var customUserAgentString: String? var zoomLevel: Double // default 1.0 var supportZoom: Boolean // default true var allowFileAccessFromFileURLs: Boolean // default false var allowUniversalAccessFromFileURLs: Boolean // default false var logSeverity: KLogSeverity // default None var backgroundColor: Color // default Transparent val androidWebSettings: PlatformWebSettings.AndroidWebSettings val desktopWebSettings: PlatformWebSettings.DesktopWebSettings val iOSWebSettings: PlatformWebSettings.IOSWebSettings val wasmJSWebSettings: PlatformWebSettings.WasmJSWebSettings } ``` Each platform applies the subset it supports. Use the nested `*WebSettings` objects for engine-specific knobs. ## Cookies ```kotlin interface CookieManager { suspend fun setCookie(url: String, cookie: Cookie) suspend fun getCookies(url: String): List suspend fun removeAllCookies() suspend fun removeCookies(url: String) } data class Cookie( val name: String, val value: String, val domain: String? = null, val path: String? = null, val expiresDate: Long? = null, val isSessionOnly: Boolean = false, val sameSite: Cookie.HTTPCookieSameSitePolicy? = null, val isSecure: Boolean? = null, val isHttpOnly: Boolean? = null, val maxAge: Long? = null, ) ``` Access the manager through `state.cookieManager`. ## Request interception ```kotlin interface RequestInterceptor { fun onInterceptUrlRequest( request: WebRequest, navigator: WebViewNavigator, ): WebRequestInterceptResult } data class WebRequest( val url: String, val headers: MutableMap = mutableMapOf(), val isForMainFrame: Boolean = false, val isRedirect: Boolean = false, val method: String = "GET", ) sealed interface WebRequestInterceptResult { data object Allow : WebRequestInterceptResult data object Reject : WebRequestInterceptResult class Modify(val request: WebRequest) : WebRequestInterceptResult } ``` Interception applies to navigator-driven main-frame navigations only. ## JS bridge ```kotlin @Immutable open class WebViewJsBridge( val navigator: WebViewNavigator? = null, val jsBridgeName: String = "kmpJsBridge", ) { fun register(handler: IJsMessageHandler) fun unregister(handler: IJsMessageHandler) fun clear() } interface IJsMessageHandler { fun methodName(): String fun handle( message: JsMessage, navigator: WebViewNavigator?, callback: (String) -> Unit, ) } @Composable fun rememberWebViewJsBridge(navigator: WebViewNavigator? = null): WebViewJsBridge ``` After each finished load (or URL change), the library injects `window.` with `callNative(methodName, params, callback)`. Register handlers before or after attach; clear happens when `WebView` leaves composition. ## What's next - [Getting started](/docs/webview/getting-started) — the same API used across a full browser chrome. - [WebView overview](/docs/webview) — supported targets and platform backends. --- # OS integration URL: https://nucleusframework.dev/en/docs/os Kotlin modules that expose native macOS, Windows, and Linux desktop capabilities such as notifications, system tray, global hotkeys, media controls, dark mode, and system colors. The OS integration modules expose native desktop capabilities from Kotlin: notifications, system tray, global hotkeys, media controls, dark mode, and system colors. Each capability is a separate Gradle artifact, so you depend only on the modules you use. ## Coverage | Feature | Module | macOS | Windows | Linux | | --- | --- | --- | --- | --- | | Cross-platform notifications | `notification-common` | yes | yes | yes | | Native notifications (per OS) | `notification-macos` / `notification-windows` / `notification-linux` | yes | yes | yes | | System tray | `composenativetray` | yes | yes | yes | | Tray-anchored popup apps | `composenativetray-app` | yes | yes | yes | | Global hotkeys | `global-hotkey` | yes | yes | yes | | Media controls | `media-control` | yes | yes | yes | | Dark mode | `darkmode-detector` | yes | yes | yes | | Accent color and high contrast | `system-color` | yes | yes | yes | | Hardware and OS info | `system-info` | yes | yes | yes | | macOS menu bar | `menu-macos` | yes | — | — | | Spell check | `spellcheck` | yes | yes | yes | | FreeDesktop icon names | `freedesktop-icons` | — | — | yes | Modules published by Nucleus use the group `dev.nucleusframework` and the artifact name `nucleus.`. The `composenativetray` module ships from a separate repository under the same group, without the `nucleus.` prefix. From 2.1.0, `composenativetray` is the icon and menu; `composenativetray-app` is `TrayApp` and depends on the Tao window backend. ## Cross-platform modules and platform modules Some capabilities come in two forms. The cross-platform modules — `notification-common`, `media-control`, and `darkmode-detector` — expose the intersection of what all three systems support behind a single API. The platform modules, such as `notification-macos`, `notification-windows`, and `notification-linux`, expose each system's native API directly, so their surfaces differ from one platform to the next. Depend on a platform module when you need a platform-specific feature, such as Windows toast progress bars, macOS interruption levels, or MPRIS volume on Linux. ## How it works Each module wraps its platform's native API through a JNI bridge that Nucleus ships and loads for you. Notifications use the UserNotifications framework (`UNUserNotificationCenter`) on macOS, WinRT toast notifications on Windows, and the FreeDesktop notification specification (`org.freedesktop.Notifications`) over D-Bus on Linux. Media controls use SMTC on Windows and MPRIS on Linux. Dark mode and accent color read reactively through `@Composable` functions, so they participate in Compose recomposition when the system state changes. ## What's next - [Cross-platform notifications](/docs/os/notifications) — send notifications from a single API on all three systems. - [System tray](/docs/os/system-tray) — Compose-rendered tray icons and menus. - [Global hotkeys](/docs/os/global-hotkey) — register shortcuts that fire even when your app is unfocused. - [Dark mode](/docs/os/dark-mode) — read the system theme and observe changes. --- # Cross-platform notifications URL: https://nucleusframework.dev/en/docs/os/notifications Send desktop notifications on macOS, Windows, and Linux from a single Kotlin API. `notification-common` lets you send a desktop notification from Kotlin without writing per-platform code. It exposes one `notification(...)` function and a `NotificationManager` that detects the current OS at runtime and delegates to the matching native backend on macOS, Windows, or Linux. The common API covers the shared subset: title, body text, a large image, a small icon, up to five action buttons, and lifecycle callbacks. For platform-specific features such as scheduling, categories, progress bars, or hero images, use the per-OS module directly. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.notification-common:") } ``` This pulls in the three platform modules transitively. Only the module matching the host OS is loaded at runtime. ## Send a notification ```kotlin import dev.nucleusframework.notification.common.NotificationManager import dev.nucleusframework.notification.common.notification NotificationManager.initialize() val n = notification( title = "Download complete", message = "report.pdf has been saved", onActivated = { openFile() }, ) { button("Open") { openFile() } button("Show in folder") { showInFolder() } } n.send() ``` `title` and `message`, along with the callbacks, are parameters of the `notification(...)` function. The trailing lambda block declares action buttons with `button(title, onClick)` and, from 2.3, optional per-platform option blocks. ## Platform-specific options Semantics that differ across OS stay in `linux { }`, `macos { }`, and `windows { }` blocks inside the builder. Each block is applied only when the notification is delivered on that OS; on every other platform it is a no-op. Shared fields (title, message, image, buttons, callbacks) remain the base. ```kotlin import dev.nucleusframework.notification.InterruptionLevel import dev.nucleusframework.notification.common.notification import dev.nucleusframework.notification.linux.Urgency import dev.nucleusframework.notification.windows.ToastDuration import dev.nucleusframework.notification.windows.ToastScenario val n = notification( title = "Build failed", message = "see logs", onActivated = { openLogs() }, ) { button("Open") { openLogs() } linux { urgency = Urgency.CRITICAL transient = true category = "device.error" } macos { interruptionLevel = InterruptionLevel.TIME_SENSITIVE relevanceScore = 0.9 subtitle = "CI" } windows { scenario = ToastScenario.URGENT duration = ToastDuration.LONG } } n.send() ``` | Block | Scope type | Properties | |-------|------------|------------| | `linux { }` | `LinuxNotificationScope` | `urgency` (`Urgency`), `category`, `transient`, `resident`, `expireTimeout` (ms; `0` = never, unset/`-1` = server default) | | `macos { }` | `MacNotificationScope` | `interruptionLevel` (`InterruptionLevel`), `relevanceScore` (`0.0..1.0`), `subtitle` | | `windows { }` | `WindowsNotificationScope` | `scenario` (`ToastScenario`), `duration` (`ToastDuration`) | `TIME_SENSITIVE` and `CRITICAL` on macOS require the matching Apple entitlement; without it the system falls back to `ACTIVE`. `ToastScenario.URGENT` needs Windows 11+ to break through Focus Assist. For deeper platform APIs (scheduling, progress bars, categories), use the [macOS](/docs/os/notification-macos), [Windows](/docs/os/notification-windows), or [Linux](/docs/os/notification-linux) modules directly. ## How it works `NotificationManager` resolves the active platform on first use and delegates to the matching native bridge. Calling `NotificationManager.initialize()` sets the subsystem up eagerly; otherwise initialization happens lazily on the first `send()`. The `notification(...)` function builds a `Notification` value. Calling `send()` on it forwards to `NotificationManager.send(notification)` and returns a `NotificationResult`. The same `Notification` instance can be sent more than once; each call produces a new OS notification. Lifecycle callbacks are not guaranteed to run on a UI thread, so marshal back to your UI dispatcher before touching Compose state. **macOS** Delegates to [`notification-macos`](/docs/os/notification-macos). Requires user authorization and a bundled, signed app. Run the packaged binary during testing (`./gradlew runDistributable`). **Windows** Delegates to [`notification-windows`](/docs/os/notification-windows), which shows WinRT toasts. **Linux** Delegates to [`notification-linux`](/docs/os/notification-linux), which uses `org.freedesktop.Notifications` over D-Bus. Works on any desktop that runs a notification daemon, including GNOME, KDE, and Cinnamon. ## API reference ### Build a notification Pass content and callbacks as function parameters, and declare buttons in the block: ```kotlin val n = notification( title = "New message from Alice", message = "Have you seen the latest build?", onActivated = { openConversation() }, onDismissed = { reason -> log.info("dismissed: $reason") }, onFailed = { log.warn("notification failed to display") }, ) { button("Reply") { /* … */ } button("Mute") { /* … */ } } ``` - `onActivated: (() -> Unit)?` — the user clicked the notification body. - `onDismissed: ((DismissReason) -> Unit)?` — the notification was dismissed. `DismissReason` is one of `USER_DISMISSED`, `TIMED_OUT`, `APPLICATION`, `UNKNOWN`. - `onFailed: (() -> Unit)?` — the notification failed to display. You can add up to five buttons; adding more throws. Platform blocks may be called more than once; later calls merge into the same scope instance for that platform. ### Send `send()` returns a `NotificationResult`: ```kotlin when (val result = n.send()) { is NotificationResult.Success -> result.handle.dismiss() is NotificationResult.Failure -> log.warn(result.reason) } ``` `NotificationResult.Success` carries a `NotificationHandle`; call `handle.dismiss()` to close the notification programmatically while it is still visible. `NotificationResult.Failure` carries a `reason` string. ### Check availability ```kotlin if (!NotificationManager.isAvailable()) { // The current OS or sandbox does not allow notifications. } ``` macOS silently drops notifications from unbundled apps. Always test against the packaged binary. ## What's next - [macOS notifications](/docs/os/notification-macos) — authorization, scheduling, categories, and attachments. - [Windows notifications](/docs/os/notification-windows) — toast content, progress bars, and history. - [Linux notifications](/docs/os/notification-linux) — urgency hints, actions, and the D-Bus daemon. --- # Notifications on macOS URL: https://nucleusframework.dev/en/docs/os/notification-macos Kotlin bindings for Apple's UserNotifications framework, covering authorization, categories, attachments, text input, schedules, and interruption levels. The `notification-macos` module lets you drive Apple's UserNotifications framework from Kotlin. You request authorization, schedule notifications, register action categories with text input, attach media, and handle user responses through `NotificationCenter` and a typed delegate. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.notification-macos:") } ``` ## Send a notification Request authorization, then submit a `NotificationRequest` with `NotificationCenter.add`: ```kotlin import dev.nucleusframework.notification.* NotificationCenter.requestAuthorization( setOf(AuthorizationOption.ALERT, AuthorizationOption.SOUND, AuthorizationOption.BADGE), ) { granted, error -> if (!granted) return@requestAuthorization NotificationCenter.add( NotificationRequest( identifier = "greeting", content = NotificationContent( title = "Hello", body = "Welcome to Nucleus", sound = NotificationSound.Default, interruptionLevel = InterruptionLevel.TIME_SENSITIVE, ), trigger = NotificationTrigger.TimeInterval(interval = 5.0), ), ) } ``` To deliver immediately, leave `trigger` unset (it defaults to `null`). macOS only delivers notifications from a bundled, bundle-identified app. During development, run `./gradlew runDistributable` or `./gradlew runGraalvmNative` — a plain `./gradlew run` produces an un-bundled JVM process that the system ignores silently. ## How it works Each API is a typed wrapper over the matching UserNotifications call, reached through a JNI bridge. `NotificationCenter` is a singleton that forwards `UNUserNotificationCenterDelegate` callbacks to the `NotificationCenterDelegate` you register. On non-macOS platforms every method is a no-op; check `NotificationCenter.isAvailable` first. Categories let you register reusable sets of action buttons — including text-input actions — ahead of time, then reference them by identifier from a `NotificationContent`. The system stores the category set, so each request only carries the `categoryIdentifier` string. Delegate callbacks are not dispatched on AWT or Swing threads. Marshal to the active UI dispatcher before you touch UI state from `willPresent` or `didReceive`. ## API reference ### Check availability ```kotlin if (NotificationCenter.isAvailable) { // running on macOS with a loaded native bridge and an app bundle } ``` ### Request authorization `requestAuthorization` takes a set of `AuthorizationOption` values and a callback reporting the result: ```kotlin NotificationCenter.requestAuthorization( setOf(AuthorizationOption.ALERT, AuthorizationOption.SOUND, AuthorizationOption.BADGE), ) { granted, error -> /* … */ } ``` Available options: `BADGE`, `SOUND`, `ALERT`, `CRITICAL_ALERT`, `PROVIDES_APP_NOTIFICATION_SETTINGS`, `PROVISIONAL`, `TIME_SENSITIVE`. ### Register categories with action buttons Build a `NotificationCategory` from `NotificationAction` or `TextInputNotificationAction` entries, then register the full set with `setNotificationCategories`: ```kotlin val category = NotificationCategory( identifier = "MESSAGE", actions = listOf( TextInputNotificationAction( identifier = "REPLY", title = "Reply", textInputButtonTitle = "Send", textInputPlaceholder = "Type a reply…", ), ), options = setOf(CategoryOption.CUSTOM_DISMISS_ACTION), ) NotificationCenter.setNotificationCategories(setOf(category)) ``` Reference the category from a notification through `NotificationContent.categoryIdentifier`. ### Attach media `NotificationAttachment.url` is a file-path string, not a `java.net.URI`: ```kotlin val request = NotificationRequest( identifier = "screenshot", content = NotificationContent( title = "Screenshot saved", body = "Click to preview", attachments = listOf( NotificationAttachment(identifier = "img", url = imageFile.absolutePath), ), ), ) ``` ### Schedule with triggers | Trigger | Use case | | --- | --- | | `trigger = null` (default) | fire now | | `NotificationTrigger.TimeInterval(interval, repeats)` | delay or recurring (repeats require `interval >= 60`) | | `NotificationTrigger.Calendar(dateComponents, repeats)` | "every Monday at 9 am" via `DateComponents` | ### Handle responses with a delegate Implement `NotificationCenterDelegate` to control foreground presentation and react to taps, action selections, and text-input responses: ```kotlin NotificationCenter.setDelegate(object : NotificationCenterDelegate { override fun willPresent(notification: DeliveredNotification): Set = setOf(PresentationOption.BANNER, PresentationOption.SOUND) override fun didReceive(response: NotificationResponse) { /* … */ } }) ``` `willPresent` returns the presentation options to use while the app is in the foreground; return an empty set to handle the notification silently. `NotificationResponse.userText` carries the text a user typed into a `TextInputNotificationAction`. ### Inspect and remove notifications `getPendingNotifications` and `getDeliveredNotifications` take callbacks and report what is queued or on screen. Remove entries with `removePendingNotifications(ids)` / `removeAllPendingNotifications()` and `removeDeliveredNotifications(ids)` / `removeAllDeliveredNotifications()`. ## Notes - Sandboxed apps need the notifications entitlement. Point the macOS `entitlementsFile` property in the Nucleus Gradle DSL at a `.entitlements` file that declares it. - For alerts that bypass Focus modes, set `interruptionLevel = InterruptionLevel.CRITICAL` and request `AuthorizationOption.CRITICAL_ALERT`. - A bundle identifier mismatch is the most common cause of silent failure — verify that `NucleusApp.appId` matches the bundle id in the packaged `.app`. ## What's next - [Cross-platform notifications](/docs/os/notifications) — the shared `NotificationManager` API. - [Notifications on Windows](/docs/os/notification-windows) — toast notifications via `WindowsNotificationCenter`. - [Notifications on Linux](/docs/os/notification-linux) — freedesktop notifications via `LinuxNotificationCenter`. - [Code signing](/docs/packaging/code-signing) — sign and bundle the app so macOS delivers its notifications. --- # Notifications on Windows URL: https://nucleusframework.dev/en/docs/os/notification-windows Send Windows toast notifications from Kotlin with adaptive layouts, images, buttons, text input, and progress bars. `notification-windows` lets you send Windows 10/11 toast notifications from Kotlin. It wraps the WinRT `ToastNotificationManager` API in a DSL that covers adaptive text, images, progress bars, selection boxes, text inputs, and headers, and works for both MSIX-packaged and unpackaged (EXE/MSI) apps. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.notification-windows:") } ``` ## Send a notification Call `initialize()` once before showing any toast, then build content with the `toast { }` DSL and pass it to `show`: ```kotlin import dev.nucleusframework.notification.windows.* WindowsNotificationCenter.initialize() WindowsNotificationCenter.show( toast { visual { text("Build finished") text("artifact-2.0.0.zip uploaded") heroImage("https://example.com/hero.png") } actions { button("Open", arguments = "open") button("Show in folder", arguments = "folder") } }, ) ``` Every method is a no-op on non-Windows platforms. Check `WindowsNotificationCenter.isAvailable` before calling if you share code across operating systems. ## How it works A toast on Windows is an XML payload dispatched through `ToastNotificationManager` and identified by an AUMID (Application User Model ID). The library resolves the AUMID for you based on how the app is packaged. - **MSIX / APPX**: the AUMID resolves automatically from the package identity. - **Unpackaged (EXE/MSI/dev)**: Nucleus derives the AUMID from `NucleusApp.aumid` and registers it on the current process. - Override it with `WindowsNotificationCenter.initialize(aumid = "Vendor.App")`. Unpackaged apps need a Start Menu `.lnk` shortcut whose AUMID property matches the one used at runtime. Without it, toasts may fail to appear or to persist in Action Center. `initialize` picks the policy automatically: `ShortcutPolicy.REQUIRE_CREATE` for installed apps and `REQUIRE_NO_CREATE` in dev mode, where no shortcut is created and a warning is logged. Pass `shortcutPolicy = ShortcutPolicy.REQUIRE_CREATE` to force it. The Nucleus Gradle plugin sets the shortcut up when you build the Windows installer formats. ## API reference ### Build adaptive content The `toast { }` builder exposes `visual`, `actions`, `audio`, and `header` blocks. Enum values are `UPPER_SNAKE_CASE`. ```kotlin toast { visual { text("Sync in progress") appLogo("logo.png", crop = AdaptiveImageCrop.CIRCLE) progressBar( status = "Uploading", title = "report.pdf", valueStringOverride = "3 of 5 files", value = 0.6, ) } actions { button("Pause", arguments = "pause") button("Cancel", arguments = "cancel", activationType = ActivationType.BACKGROUND) } audio(ToastAudioSource.REMINDER) scenario = ToastScenario.REMINDER } ``` `ToastActionsBuilder` provides `textBox`, `selectionBox`, `button`, and `contextMenuItem`. The `status` string is the only required argument of `progressBar`. ### Listen for events Register a listener with `addListener`. The callbacks receive flat parameters, not event-argument objects: ```kotlin WindowsNotificationCenter.addListener(object : ToastNotificationListener { override fun onActivated( tag: String, group: String, arguments: String, userInputs: Map, ) { /* ... */ } override fun onDismissed(tag: String, group: String, reason: DismissalReason) { /* ... */ } override fun onFailed(tag: String, group: String, errorCode: Int) { /* ... */ } }) ``` Remove a listener with `WindowsNotificationCenter.removeListener(listener)`. ### Update an existing toast Show a toast with a `tag` and `group`, then push new data-bound values with `update`. This drives progress bars without replacing the toast: ```kotlin WindowsNotificationCenter.update( tag = "upload", group = "transfers", data = ToastNotificationData( sequenceNumber = 1, values = mapOf("progressValue" to "0.8"), ), ) ``` `ToastNotificationData` takes `sequenceNumber` first (a monotonically increasing number that avoids races) and the `values` map second. ### Read the Action Center history `getHistory` delivers the toasts currently in Action Center through a callback. Each `HistoryEntry` exposes `tag` and `group`: ```kotlin WindowsNotificationCenter.getHistory { entries, error -> entries.forEach { entry -> println("${entry.tag} / ${entry.group}") } } ``` ### Remove toasts - `remove(tag, group)` removes a single toast. - `removeGroup(group)` removes every toast in a group. - `clearAll()` clears the app's Action Center entries. - `uninitialize()` releases native resources on shutdown. ## Notes - Keep the AUMID consistent across the Start Menu shortcut, your `nucleus { }` Gradle config, and runtime. A mismatch is the usual cause of a toast that appears once and then never again. - `tag` and `group` are limited to 16 characters each. - Hero images and app logos accept remote `http(s)://` URLs in unpackaged apps as long as the URL resolves quickly. For offline-safe behavior, ship a local file via `ms-appx:///` (MSIX) or `file:///` (unpackaged). ## What's next - [Notifications overview](/docs/os/notifications) — the cross-platform notification DSL. - [Notifications on macOS](/docs/os/notification-macos) — `UNUserNotificationCenter` from Kotlin. - [Notifications on Linux](/docs/os/notification-linux) — Freedesktop notifications over D-Bus. --- # Notifications on Linux URL: https://nucleusframework.dev/en/docs/os/notification-linux Send freedesktop desktop notifications over D-Bus from Kotlin, with urgency, hints, sounds, and action callbacks. `notification-linux` sends desktop notifications through the `org.freedesktop.Notifications` D-Bus service, which GNOME, KDE, Cinnamon, Xfce, and other desktops implement. It maps the freedesktop Desktop Notifications specification to Kotlin over GIO/GDBus, with no JNA or Java D-Bus dependency. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.notification-linux:") } ``` This pulls in [`freedesktop-icons`](/docs/os/freedesktop-icons) transitively for typesafe icon names. ## Send a notification Build a `Notification` and pass it to `LinuxNotificationCenter.notify`, which returns the server-assigned ID: ```kotlin import dev.nucleusframework.notification.linux.LinuxNotificationCenter import dev.nucleusframework.notification.linux.Notification import dev.nucleusframework.notification.linux.NotificationAction import dev.nucleusframework.notification.linux.NotificationHints import dev.nucleusframework.notification.linux.Urgency import dev.nucleusframework.freedesktop.icons.FreedesktopIcon val id: Int = LinuxNotificationCenter.notify( Notification( appName = "MyApp", summary = "Build finished", body = "myapp-2.0.0.zip is ready", appIcon = FreedesktopIcon.Status.DIALOG_INFORMATION, hints = NotificationHints(urgency = Urgency.NORMAL), actions = listOf(NotificationAction("open", "Open")), ), ) ``` `notify` returns an ID greater than `0` on success, or `0` on failure. Check `LinuxNotificationCenter.isAvailable` before sending. It returns `false` on non-Linux platforms or when the native library could not be loaded. ## How it works `LinuxNotificationCenter` talks to `org.freedesktop.Notifications` over D-Bus through JNI. Each call to `notify` invokes the `Notify` method and returns the ID the server assigns. Passing a non-zero `replacesId` atomically replaces an existing notification with that ID. Action clicks and close events arrive later as D-Bus signals. Register a `LinuxNotificationListener` to receive them; signal monitoring starts when the first listener is added and stops when the last is removed. Callbacks are dispatched on the Swing EDT. The specification is uniform, but servers differ in what they render. GNOME Shell in particular is unreliable about `appIcon`; set `hints.imagePath` for consistent iconography. Call `getServerInformation()` at startup to log the active server, and `getCapabilities()` to see what the server supports. ## API reference ### Set urgency ```kotlin hints = NotificationHints(urgency = Urgency.CRITICAL) ``` `Urgency` is `LOW`, `NORMAL`, or `CRITICAL`. Critical notifications typically bypass do-not-disturb and do not auto-expire. ### Add action buttons Each `NotificationAction` has a `key` and a `label`. Use `NotificationAction.DEFAULT_KEY` for the action bound to clicking the notification body. ```kotlin val notification = Notification( appName = "MyApp", summary = "New message", actions = listOf( NotificationAction("reply", "Reply"), NotificationAction("archive", "Archive"), ), ) ``` ### Receive callbacks ```kotlin import dev.nucleusframework.notification.linux.CloseReason import dev.nucleusframework.notification.linux.LinuxNotificationListener LinuxNotificationCenter.addListener(object : LinuxNotificationListener { override fun onActionInvoked(notificationId: Int, actionKey: String) { /* … */ } override fun onClosed(notificationId: Int, reason: CloseReason) { /* … */ } }) ``` `CloseReason` is `EXPIRED`, `DISMISSED`, `CLOSED`, or `UNDEFINED`. Remove a listener with `removeListener`. ### Embed inline image data Use `ImageData` when you have raw pixels rather than a file path — for example, a generated avatar. The bytes must be in RGB or RGBA order: ```kotlin import dev.nucleusframework.notification.linux.ImageData hints = NotificationHints( imageData = ImageData( width = width, height = height, rowstride = width * 4, hasAlpha = true, data = bytes, ), ) ``` `channels` defaults to `4` when `hasAlpha` is `true` and `3` otherwise, and `bitsPerSample` defaults to `8`. ### Play a sound ```kotlin import dev.nucleusframework.notification.linux.NotificationSound hints = NotificationHints( soundName = NotificationSound.Notification.DIALOG_INFORMATION, ) ``` Sound names are grouped by category (`Alert`, `Notification`, `Action`, `InputFeedback`, `Game`); use `NotificationSound.Custom("x-myapp-…")` for names outside the specification. ### Close a notification ```kotlin LinuxNotificationCenter.closeNotification(id) ``` ## Notes - Under `Xvfb` or a headless CI, `notify` returns without error, but no daemon displays the notification. - If notifications work in development but not under Flatpak, check the `org.freedesktop.Notifications` portal permission in your manifest. ## What's next - [freedesktop-icons](/docs/os/freedesktop-icons) — typesafe icon names for `appIcon` and `imagePath`. - [Notifications overview](/docs/os/notifications) — the cross-platform notification API. - [Notifications on macOS](/docs/os/notification-macos) and [Windows](/docs/os/notification-windows). --- # System tray URL: https://nucleusframework.dev/en/docs/os/system-tray Render a tray icon and a native context menu from Compose on macOS, Windows, and Linux. The system tray API draws a tray icon and its native context menu from Compose on macOS, Windows, and Linux. The icon is anything Compose can draw, the menu is a Kotlin DSL, and both react to Compose state. `Tray()` is a single composable that hands the rendered icon and menu to the native tray API of the current OS. System tray ships from [`NucleusFramework/ComposeNativeTray`](https://github.com/NucleusFramework/ComposeNativeTray) with its own release cycle. Pin it separately from Nucleus. From 2.1.0 the library is two artifacts: - `dev.nucleusframework:composenativetray` — `Tray` and the menu DSL. Works in Compose Desktop's `application { }` and in `nucleusApplication { }`. - `dev.nucleusframework:composenativetray-app` — `TrayApp`. Requires `nucleusApplication { }`. See [Tray apps](/docs/os/tray-app). ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:composenativetray:") } ``` Add `composenativetray-app` only when you use [`TrayApp`](/docs/os/tray-app). ## Add a tray icon `Tray` is a plain composable. Call it from `nucleusApplication` (or from Compose Desktop's `application`): ```kotlin import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.composenativetray.tray.api.Tray import kotlin.system.exitProcess fun main() = nucleusApplication { Tray( icon = Icons.Default.Favorite, tooltip = "My App", primaryAction = { showWindow() }, ) { Item(label = "Show window") { showWindow() } Divider() Item(label = "Quit") { exitProcess(0) } } } ``` ## How it works `Tray()` is a composable. Its icon is rasterised from Compose whenever the slot's state changes, then handed to the native tray API of the current OS. The menu lambda runs inside a DSL scope that re-evaluates on recomposition, so adding, removing, or relabelling items follows the usual Compose recomposition rules. The tray participates in your Compose state tree: it updates through recomposition, without explicit rebuild calls or separate `.ico`/`.png`/`.icns` files to maintain. Each platform maps to its native tray mechanism: **macOS** Backed by `NSStatusItem` in the menu bar. Vector icons render at native DPI, with optional tinting via the `tint` parameter. **Windows** Backed by `Shell_NotifyIcon`. Vector input is rasterised to a 32×32 `.ico` per scale factor. Pass a hand-authored `.ico` via `windowsIcon` for exact control. **Linux (GNOME)** Uses StatusNotifierItem (KStatusNotifierItem). On stock GNOME this requires the AppIndicator extension; on Ubuntu it works out of the box. **Linux (KDE)** StatusNotifierItem is native to Plasma. Hover tooltips and badges work where Plasma supports them. ## Set the icon source The `icon` parameter accepts an `ImageVector`, a `Painter`, or a Compose Multiplatform `DrawableResource`. For full control, use `iconContent` to draw the icon with any composable: ```kotlin // ImageVector Tray(icon = Icons.Default.Notifications, tint = Color.White, tooltip = "App") { /* … */ } // Painter Tray(icon = painterResource("icon.png"), tooltip = "App") { /* … */ } // Compose Multiplatform DrawableResource Tray(icon = Res.drawable.app_icon, tooltip = "App") { /* … */ } // Fully custom — any composable Tray( iconContent = { Box(Modifier.size(24.dp).background(Color.Red, CircleShape)) { Text("3", color = Color.White, modifier = Modifier.align(Alignment.Center)) } }, tooltip = "3 notifications", ) { /* … */ } ``` ## Use platform-specific icons To use a native `.ico` on Windows and a vector elsewhere, set `windowsIcon` and `macLinuxIcon`: ```kotlin Tray( windowsIcon = painterResource("icon.ico"), macLinuxIcon = Icons.Default.Notifications, tint = Color.White, tooltip = "My App", ) { /* menu */ } ``` ## Handle the primary action `primaryAction` fires on left-click (macOS/Windows) or single click (Linux, desktop-dependent). Without it, the menu opens on every click. ```kotlin Tray( icon = Icons.Default.Favorite, tooltip = "My App", primaryAction = { showWindow() }, ) { Item(label = "Quit") { exitProcess(0) } } ``` ## What's next - [Tray menu DSL](/docs/os/tray-menu-dsl) — items, checkables, submenus, dividers, and reactive composition. - [Tray apps](/docs/os/tray-app) — make the tray icon open a Compose popup window. - [WebView](/docs/webview) — another Nucleus component shipped from its own repository. - [PDF reader](/docs/pdf) — one PDF API across Android, iOS, web, and desktop. --- # Tray menu DSL URL: https://nucleusframework.dev/en/docs/os/tray-menu-dsl Build the tray context menu from Kotlin with items, checkable items, submenus, dividers, and conditional content. The tray menu DSL lets you build the tray context menu from Kotlin, inside the `Tray()` trailing lambda. It ships in `composenativetray` — no extra artifact. The menu participates in Compose recomposition: when a `mutableStateOf` changes, the labels, checkmarks, icons, and entire branches update in the native menu. The DSL provides four item types — `Item`, `CheckableItem`, `SubMenu`, and `Divider`. Every item type accepts an icon as an `ImageVector`, `Painter`, `DrawableResource`, or `@Composable`. Because the menu is ordinary Kotlin code, you can use `if` branches and computed values directly. ## Add menu items Add an `Item` for each action. Pass `isEnabled = false` to disable an item, and `icon` to show a leading icon: ```kotlin Tray(icon = Icons.Default.Favorite, tooltip = "App") { Item(label = "Open settings") { openSettings() } Item(label = "Disabled", isEnabled = false) {} Item(label = "Export", icon = Icons.Default.Upload) { export() } } ``` ## Add checkable items Use `CheckableItem` for toggles. Each platform renders its native checkmark style. The `checked` parameter reflects the current state, and the trailing lambda receives the new value: ```kotlin var notifications by remember { mutableStateOf(true) } var darkMode by remember { mutableStateOf(false) } Tray(icon = Icons.Default.Favorite, tooltip = "App") { CheckableItem(label = "Notifications", checked = notifications, onCheckedChange = { notifications = it }) CheckableItem(label = "Dark mode", icon = Icons.Default.DarkMode, checked = darkMode, onCheckedChange = { darkMode = it }) } ``` ## Add submenus Nest a `SubMenu` to group related items. Submenus can nest to any depth: ```kotlin Tray(icon = Icons.Default.Favorite, tooltip = "App") { SubMenu(label = "Tools", icon = Icons.Default.Build) { Item(label = "Terminal") { openTerminal() } Item(label = "File manager") { openFiles() } SubMenu(label = "More") { Item(label = "Calculator") { openCalc() } } } } ``` ## Add dividers Insert a `Divider` to separate groups of items: ```kotlin Tray(icon = Icons.Default.Favorite, tooltip = "App") { Item(label = "Show window") { show() } Divider() Item(label = "Settings") { settings() } Item(label = "About") { about() } Divider() Item(label = "Quit") { exitProcess(0) } } ``` ## Build a reactive menu Because the DSL is Compose code, you can show items conditionally and compute labels from state. Write the menu like any other Compose tree: ```kotlin var isConnected by remember { mutableStateOf(false) } var showAdvanced by remember { mutableStateOf(false) } Tray(icon = Icons.Default.Wifi, tooltip = "Network") { Item(label = if (isConnected) "Disconnect" else "Connect") { isConnected = !isConnected } CheckableItem(label = "Advanced options", checked = showAdvanced, onCheckedChange = { showAdvanced = it }) if (showAdvanced) { Divider() SubMenu(label = "Advanced") { Item(label = "DNS settings") {} Item(label = "Proxy") {} } } Divider() Item(label = "Quit") { exitProcess(0) } } ``` ## How it works The native menu rebuilds itself when the state it reads mutates. You do not call `refresh()` and you do not diff the menu yourself — Compose tracks the state reads and Nucleus applies the changes to the platform menu. ## What's next - [System tray](/docs/os/system-tray) — icon types, primary action, and platform-specific icons. - [Tray apps](/docs/os/tray-app) — show a popup window instead of, or alongside, the menu. --- # Tray-anchored apps URL: https://nucleusframework.dev/en/docs/os/tray-app TrayApp anchors a Compose popup window to a system tray icon so you can build menu bar apps in Kotlin. `TrayApp` anchors a full Compose window to a system tray icon: left-click the icon to open the window beside it, and click away to close it. It implements the menu bar app pattern used by tools such as Bartender, iStat Menus, and Hidden Bar. From 2.1.0, `TrayApp` lives in `composenativetray-app`, not in the core tray artifact. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:composenativetray-app:") } ``` `composenativetray-app` pulls in [`composenativetray`](/docs/os/system-tray) and the Nucleus Tao window backend. `TrayApp` is an extension on `NucleusApplicationScope` — launch with `nucleusApplication { }`. ## Create a tray app Pass an icon, a tooltip, a window size, and the Compose content to render in the popup: ```kotlin import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Dashboard import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.composenativetray.trayapp.TrayApp fun main() = nucleusApplication { TrayApp( icon = Icons.Default.Dashboard, tooltip = "Quick dashboard", windowSize = DpSize(300.dp, 400.dp), ) { Column(Modifier.fillMaxSize().padding(16.dp)) { Text("Dashboard", style = MaterialTheme.typography.h6) Spacer(Modifier.height(8.dp)) Text("CPU: 42%") Text("RAM: 8.2 GB") } } } ``` ## How it works `TrayApp` is a tray icon plus a transparent, undecorated, always-on-top Compose window. The library tracks the tray icon's screen position and places the window next to it with the correct offset for each operating system. Click-outside-to-dismiss is wired to the OS focus-loss event, so the popup closes the way native menu bar windows do on each desktop. ## Control visibility and size with state Hold a `TrayAppState` to show, hide, resize, or change the dismiss mode from your own code: ```kotlin import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Dashboard import androidx.compose.foundation.layout.Column import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.composenativetray.trayapp.TrayApp import dev.nucleusframework.composenativetray.trayapp.TrayWindowDismissMode import dev.nucleusframework.composenativetray.trayapp.rememberTrayAppState fun main() = nucleusApplication { val state = rememberTrayAppState( initialWindowSize = DpSize(350.dp, 500.dp), initiallyVisible = false, initialDismissMode = TrayWindowDismissMode.AUTO, ) TrayApp( icon = Icons.Default.Dashboard, tooltip = "Dashboard", state = state, ) { Column { Text("Dashboard") Button(onClick = { state.hide() }) { Text("Close") } Button(onClick = { state.setWindowSize(500.dp, 600.dp) }) { Text("Resize") } } } } ``` ## Add a context menu Left-click opens the popup; right-click opens a classic menu: ```kotlin import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Dashboard import dev.nucleusframework.application.nucleusApplication import dev.nucleusframework.composenativetray.trayapp.TrayApp import kotlin.system.exitProcess fun main() = nucleusApplication { TrayApp( icon = Icons.Default.Dashboard, tooltip = "Dashboard", menu = { Item(label = "Settings") { openSettings() } Divider() Item(label = "Quit") { exitProcess(0) } }, ) { Text("Popup content") } } ``` ## Hide the Dock icon on macOS On the Tao backend, do not set `LSUIElement` in `Info.plist`. Pass `dockIconFollowsWindows = true` to `nucleusApplication` instead: ```kotlin fun main() = nucleusApplication(dockIconFollowsWindows = true) { TrayApp( icon = Icons.Default.Dashboard, tooltip = "Dashboard", ) { Text("Popup content") } } ``` The app starts as an accessory (no Dock icon, no menu bar). A Dock tile appears only while at least one `DecoratedWindow` with `hiddenFromDock = false` is visible. `TrayApp` popups are standalone panels and never count, so a tray-only app stays out of the Dock. The flag is ignored off macOS and on the AWT backend. See [Hiding from the taskbar/Dock](/docs/tao/decorated-window#hiding-from-the-taskbardock). ## API reference ### TrayApp window options | Parameter | Default | Notes | | --- | --- | --- | | `windowSize` | `DpSize(300.dp, 200.dp)` | initial size | | `visibleOnStart` | `false` | show immediately | | `enterTransition` / `exitTransition` | platform default | animations | | `undecorated` | `true` | no chrome | | `resizable` | `false` | user resize | | `windowsTitle` | `""` | window title (used on Linux and when decorated) | | `horizontalOffset` / `verticalOffset` | 0 / platform default | nudge against the tray anchor | ### TrayAppState | API | Description | | --- | --- | | `isVisible: StateFlow` | current visibility | | `show() / hide() / toggle()` | imperative control | | `setWindowSize(size)` | resize on the fly | | `setDismissMode(mode)` | `AUTO` (click outside closes) or `MANUAL` | | `onVisibilityChanged(cb)` | observe transitions | ## Notes - From 2.1.0, `getTrayPosition()` and `TrayPosition` live in `composenativetray-app` (`dev.nucleusframework.composenativetray.trayapp`). They need the Tao backend for screen geometry. Prefer `TrayApp` if you only need a popup next to the icon. - The popup is a standalone native panel, not a `DecoratedWindow`. File drops reach Compose `dragAndDropTarget` the same way as a regular window (Windows and macOS from 2.5.3, Linux from 2.5.4). Wheel and trackpad scroll uses the same AWT mapping as the main window (Windows from 2.5.2, macOS from 2.5.4, Linux from 2.5.6). A hidden Windows popup no longer submits GPU frames (2.5.2). A frame scheduled from inside a scene pass — a scrollbar drag subcomposing new lazy items, for example — is posted to the main queue instead of rendered inline, so it cannot re-enter measure and layout (2.5.7). - On macOS the panel is stationary and joins all spaces, so "Click wallpaper to reveal desktop" on Sonoma and later leaves it in place and `TrayApp` dismisses it itself (2.5.5). Clicks on the status item and on its context menu do not count as clicks outside the popup, so a right-click opens the menu instead of hiding the popup (2.5.5). ## What's next - [System tray](/docs/os/system-tray) — the tray icon API that `TrayApp` builds on. - [Tray menu DSL](/docs/os/tray-menu-dsl) — build the right-click menu. - [Single instance](/docs/lifecycle/single-instance) — re-launching from a `.app` re-shows the popup instead of starting a second process. --- # Global hotkeys URL: https://nucleusframework.dev/en/docs/os/global-hotkey Register OS-wide keyboard shortcuts that fire even when your app does not have focus. `global-hotkey` lets you register system-wide keyboard shortcuts from Kotlin on macOS, Windows, and Linux. A global hotkey fires when its key combination is pressed, regardless of which window has focus, which is what media players, screen-capture tools, and accessibility utilities rely on. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.global-hotkey:") } ``` ## Register a hotkey Initialize the manager once, register a combination, and keep the returned handle to unregister it later: ```kotlin import dev.nucleusframework.globalhotkey.GlobalHotKeyManager import dev.nucleusframework.globalhotkey.HotKeyModifier import java.awt.event.KeyEvent GlobalHotKeyManager.initialize() val handle = GlobalHotKeyManager.register( keyCode = KeyEvent.VK_F12, modifiers = HotKeyModifier.CONTROL + HotKeyModifier.SHIFT, ) { keyCode, modifiers -> println("Hotkey pressed") } // later GlobalHotKeyManager.unregister(handle) GlobalHotKeyManager.shutdown() ``` `register` returns a `Long` handle, or `-1` if the binding could not be registered. The listener receives the `keyCode` and `modifiers` bitmask that triggered it. To scope a hotkey to a Composable's lifetime, register in `DisposableEffect` and unregister in `onDispose`: ```kotlin DisposableEffect(Unit) { GlobalHotKeyManager.initialize() val handle = GlobalHotKeyManager.register( keyCode = KeyEvent.VK_K, modifiers = 0 + HotKeyModifier.META, ) { _, _ -> showQuickSwitcher() } onDispose { GlobalHotKeyManager.unregister(handle) } } ``` ## How it works `GlobalHotKeyManager` is a thread-safe singleton. `initialize()` loads the native bridge for the current platform and starts its message loop: `RegisterHotKey` on Windows, Carbon `RegisterEventHotKey` on macOS, and `XGrabKey` (X11) or the `org.freedesktop.portal.GlobalShortcuts` D-Bus portal (Wayland) on Linux. Each `register(...)` call maps the portable `modifiers` bitmask to the native modifier flags and returns a handle you pass back to `unregister`. When a registration fails, `register` returns `-1` and sets `lastError` to a description of the failure. Listeners are invoked from the native message loop, not the UI thread. Marshal back to your UI or audio thread yourself before touching shared state. ### Linux portal registrations On Wayland, the portal persists grants by `(app_id, shortcut_id)`. From 2.3 the Linux path derives a **stable** `shortcut_id` from modifiers + keyCode (`nucleus_m{mods}_k{keyCode}`) so the same chord keeps the same id across launches and registration order — without that, each start re-prompted the user. `register(...)` calls are also **debounced** into a single `BindShortcuts` dialog. After a loop of registrations, either wait for the flush or call `commitRegistrations()` to force it: ```kotlin val a = GlobalHotKeyManager.register(KeyEvent.VK_K, 0 + HotKeyModifier.CONTROL) { _, _ -> /* … */ } val b = GlobalHotKeyManager.register(KeyEvent.VK_L, 0 + HotKeyModifier.CONTROL) { _, _ -> /* … */ } GlobalHotKeyManager.commitRegistrations() // one portal dialog for both // Debug: portal id for a handle (Linux only; null elsewhere) val id: String? = GlobalHotKeyManager.portalShortcutId(a) ``` ## API reference ### Register a key combination ```kotlin fun register( keyCode: Int, modifiers: Int = 0, description: String? = null, listener: HotKeyListener, ): Long ``` `keyCode` is an AWT virtual key code such as `KeyEvent.VK_F12`. `modifiers` is a bitmask built from `HotKeyModifier` values; use `0` for no modifiers. `description` is a user-readable label shown in the system shortcut dialog on Linux/Wayland and ignored elsewhere. Returns the registration handle, or `-1` on failure. ### Combine modifiers `HotKeyModifier` has four values: `ALT`, `CONTROL`, `SHIFT`, and `META`. On macOS, `ALT` is Option and `META` is Command; on Windows and Linux, `META` is the Win/Super key. Combine them with `+`, which produces the `Int` bitmask that `register` expects: ```kotlin val modifiers = HotKeyModifier.CONTROL + HotKeyModifier.SHIFT ``` ### Handle the callback `HotKeyListener` is a functional interface with a single method: ```kotlin fun onHotKey(keyCode: Int, modifiers: Int) ``` Because it is a `fun interface`, you can pass a two-parameter lambda wherever a `HotKeyListener` is expected. ### Register a media key ```kotlin GlobalHotKeyManager.register(MediaKey.PLAY_PAUSE) { _, _ -> player.togglePlayPause() } GlobalHotKeyManager.register(MediaKey.NEXT_TRACK) { _, _ -> player.skipNext() } ``` `MediaKey` has four values: `PLAY_PAUSE`, `STOP`, `NEXT_TRACK`, and `PREV_TRACK`. Media keys are not supported on macOS. On macOS this overload returns `-1` and sets `lastError` to `"Media keys are not supported on macOS"`. ### Check availability ```kotlin if (!GlobalHotKeyManager.isAvailable) { // The native bridge is not loaded on this platform. } ``` `isAvailable` reports whether the native library is loaded and functional on the current platform. ### Unregister and shut down `unregister(handle: Long): Boolean` releases a single binding and returns whether it succeeded. `shutdown()` unregisters every hotkey and stops the native message loop; call it on app exit. ## Notes On macOS the user may be prompted to grant your app **Input Monitoring** (and sometimes **Accessibility**) under System Settings → Privacy & Security. Until they accept, callbacks never fire. Link to `x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent` from your prompt. - Linux Wayland compositors generally disallow global key grabs from arbitrary apps. The library uses the `org.freedesktop.portal.GlobalShortcuts` portal where available, which requires a reverse-DNS `.desktop` file and the app launched from it; on other compositors the binding may not fire. - Read `GlobalHotKeyManager.lastError` after a failed call to find out why a registration was rejected. ## What's next - [Media control](/docs/os/media-control) — expose playback state and respond to system media events. - [System info](/docs/os/system-info) — query OS, CPU, memory, and battery details from Kotlin. - [OS integration overview](/docs/os) — the full set of platform integration modules. --- # Media controls URL: https://nucleusframework.dev/en/docs/os/media-control Publish now-playing metadata to the system media center and handle playback commands on macOS, Windows, and Linux from Kotlin. Media controls lets you publish track metadata to the operating system's media center and receive playback commands from Kotlin. A single `MediaControlService` object wraps the MPRIS D-Bus interface on Linux, `MPNowPlayingInfoCenter` / `MPRemoteCommandCenter` on macOS, and the System Media Transport Controls (SMTC) on Windows. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.media-control:") } ``` ## Publish now-playing metadata Check that the native backend loaded, configure the player identity, then push metadata and playback state: ```kotlin import dev.nucleusframework.media.control.MediaControlService import dev.nucleusframework.media.control.MediaMetadata import dev.nucleusframework.media.control.MediaPlaybackState import dev.nucleusframework.media.control.MediaPlaybackStatus if (MediaControlService.isAvailable()) { MediaControlService.configure() MediaControlService.setMetadata( MediaMetadata( title = "Strobe", artist = "deadmau5", album = "For Lack of a Better Name", coverUrl = "file:///tmp/cover.png", duration = 10 * 60 * 1_000L, ), ) MediaControlService.setPlaybackState( MediaPlaybackState(status = MediaPlaybackStatus.PLAYING, positionMs = 0), ) } ``` `duration` and `positionMs` are in milliseconds. On Linux, a `coverUrl` pointing at a local file must use the `file://` scheme. On Windows, a local cover (`file://` or a bare path) appears in the SMTC flyout; remote `http`/`https` URLs work as well. ## Handle playback commands Register a listener with `attach` to receive commands from the OS media center. The callback is dispatched on the Swing EDT, so you can mutate Compose or Swing state directly: ```kotlin import dev.nucleusframework.media.control.MediaControlEvent MediaControlService.attach { event -> when (event) { MediaControlEvent.Play -> player.play() MediaControlEvent.Pause -> player.pause() MediaControlEvent.Toggle -> player.toggle() MediaControlEvent.Next -> player.skipToNext() MediaControlEvent.Previous -> player.skipToPrevious() MediaControlEvent.Stop -> player.stop() is MediaControlEvent.SeekBy -> player.seekBy(event.offsetMs) is MediaControlEvent.SetPosition -> player.seekTo(event.positionMs) else -> {} } } ``` Only one listener is active at a time — calling `attach` again replaces the previous one. Call `detach()` to stop receiving events and unregister from the media center. ## How it works `MediaControlService` is a singleton that delegates to a per-OS native bridge. `isAvailable()` returns `true` only when that bridge loaded successfully on the current platform; when it returns `false`, every call is a no-op, so you can guard setup with a single check. `configure()` sets the player identity. On Linux it registers the MPRIS bus name (default `org.mpris.MediaPlayer2.`, where `` is `NucleusApp.appId` sanitized into a legal D-Bus name); on macOS the identity comes from the host app bundle; on Windows it uses the application's AUMID. Both parameters are defaulted, so `configure()` with no arguments is the common case. After configuring, push metadata and playback state as your player changes. Incoming commands are parsed from the native bridge and delivered to your listener on the Swing EDT. The commands the OS can send back differ by platform: - **Linux (MPRIS)** emits every `MediaControlEvent` variant. - **macOS** emits `Play`, `Pause`, `Toggle`, `Next`, `Previous`, `Stop`, and `SetPosition`. - **Windows (SMTC)** emits `Play`, `Pause`, `Next`, `Previous`, `Stop`, `SeekBy`, and `SetPosition`. ## API reference ### Set metadata ```kotlin MediaControlService.setMetadata( MediaMetadata( title = "Track", artist = "Artist", album = "Album", coverUrl = "file:///tmp/cover.png", duration = 3 * 60 * 1_000L, ), ) ``` Every `MediaMetadata` field is nullable and defaults to `null`. `duration` is in milliseconds. ### Set playback state ```kotlin MediaControlService.setPlaybackState( MediaPlaybackState( status = MediaPlaybackStatus.PLAYING, positionMs = currentPositionMs, ), ) ``` `status` is one of `MediaPlaybackStatus.STOPPED`, `PAUSED`, or `PLAYING`. `positionMs` is optional and defaults to `null`. Push a state update on every play, pause, stop, and seek so the system seek bar stays accurate. ### Set volume ```kotlin MediaControlService.setVolume(0.5) ``` `volume` is clamped to the range `0.0`–`1.0`. ### Attach and detach ```kotlin MediaControlService.attach { event -> /* handle */ } MediaControlService.detach() ``` ### Per-platform notes **macOS** Set `LSApplicationCategoryType` to `public.app-category.music` in `Info.plist` so the Now Playing widget treats the app as a media player. Hardware media keys deliver Play, Pause, Next, and Previous when the app is the active now-playing client. **Windows** SMTC integration is automatic once you call `configure()`. The system treats the most recent publisher as the current now-playing source, and the identity resolves through the app's AUMID. A local `coverUrl` (`file://` or a bare path) shows in the flyout; `http`/`https` URLs are fetched by the system. **Linux** `configure()` registers the MPRIS bus name `org.mpris.MediaPlayer2.`, where `` is `NucleusApp.appId` sanitized into a legal D-Bus name: illegal characters become `_`, elements that start with a digit get an `_` prefix, empty elements are dropped, and the suffix is truncated to keep the name under the 255-character D-Bus limit. `playerctl` and desktop indicators discover the player by enumerating that prefix. Pass a custom `dbusName` to override it. ## Notes - `setVolume(...)` is a no-op on macOS — Now Playing has no per-app volume channel. - On Linux, `isAvailable()` only reports whether the native library loaded. If the session bus is unreachable or the MPRIS name is invalid, registration is skipped: `attach` returns without blocking and logs a warning, while `isAvailable()` still returns `true`. ## What's next - [Global hotkeys](/docs/os/global-hotkey) — bind media keys and other shortcuts system-wide. - [System info](/docs/os/system-info) — query the OS, CPU, battery, and displays from Kotlin. - [Quickstart](/docs/start/quickstart) — build and run your first Nucleus app. --- # Dark mode detector URL: https://nucleusframework.dev/en/docs/os/dark-mode Detect the operating system's dark mode from Kotlin and recompose your Compose UI when the theme changes. `darkmode-detector` reports whether the operating system is in dark mode and notifies you when that changes. It provides an `isSystemInDarkMode()` composable that recomposes on theme changes, plus an imperative `IDarkModeDetector` for use outside Compose. macOS, Windows, and Linux are supported. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.darkmode-detector:") } ``` ## Detect dark mode in Compose Call `isSystemInDarkMode()` and use the result to select your color scheme. The composable recomposes when the system theme changes: ```kotlin import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import dev.nucleusframework.darkmodedetector.isSystemInDarkMode @Composable fun App() { val isDark = isSystemInDarkMode() MaterialTheme( colorScheme = if (isDark) darkColorScheme() else lightColorScheme(), ) { // Content recomposes when the OS theme flips. } } ``` Compose Desktop's own `isSystemInDarkTheme()` only reads `LocalSystemTheme`, whose default is a non-reactive Skiko snapshot — it does not update when the user switches themes at runtime. `isSystemInDarkMode()` returns the current value and registers a listener that triggers recomposition on every change. ### Bridge to Compose `isSystemInDarkTheme` From 2.3, `nucleusApplication` provides Compose's `LocalSystemTheme` from `isSystemInDarkMode()` on both AWT and Tao entry points (Tao windows inherit it through the scene local bridge). Official `androidx.compose.foundation.isSystemInDarkTheme()` — and any library that calls it — then tracks live OS theme changes under the DSL, without calling Nucleus APIs at every call site. `darkmode-detector` is an **`api`** dependency of `nucleus-application`, so the detector is on the consumer classpath whenever you use the application entry point. You still add the coordinate explicitly when you use the detector outside `nucleusApplication`. ```kotlin import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import dev.nucleusframework.application.nucleusApplication fun main() = nucleusApplication { // isSystemInDarkTheme() recomposes when the OS theme flips val dark = isSystemInDarkTheme() MaterialTheme(colorScheme = if (dark) darkColorScheme() else lightColorScheme()) { DecoratedWindow(onCloseRequest = ::exitApplication) { /* … */ } } } ``` Use `isSystemInDarkMode()` when you need Nucleus's API outside Compose or when you are not inside `nucleusApplication`. Prefer `isSystemInDarkTheme()` for stock Material / third-party composables that already depend on it. Inside a `@Preview` (when `LocalInspectionMode` is `true`), `isSystemInDarkMode()` delegates to Compose's `isSystemInDarkTheme()` so previews render without a running detector. ## How it works Each platform reads the system theme through a native JNI bridge and watches the OS for changes. No polling is involved; the detector reacts to change notifications from the OS itself. - **macOS**: reads the `AppleInterfaceStyle` system preference and registers an observer through `NSDistributedNotificationCenter`. - **Windows**: reads the registry value `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme` and watches it with `RegNotifyChangeKeyValue` on a native background thread. - **Linux**: reads the XDG Desktop Portal `color-scheme` preference over the `org.freedesktop.portal.Settings` D-Bus interface; a value of `1` means prefer-dark. On unsupported platforms, `getPlatformDarkModeDetector()` returns `NoopDarkModeDetector`, which always reports light and ignores listeners. ## API reference ### Read the current theme `getPlatformDarkModeDetector()` returns the `IDarkModeDetector` for the current platform. Call `isDark()` for a one-shot read: ```kotlin import dev.nucleusframework.darkmodedetector.getPlatformDarkModeDetector val detector = getPlatformDarkModeDetector() val dark: Boolean = detector.isDark() ``` ### Listen for changes Register a `java.util.function.Consumer` to be notified when the theme changes, and remove it when you are done: ```kotlin import dev.nucleusframework.darkmodedetector.getPlatformDarkModeDetector import java.util.function.Consumer val detector = getPlatformDarkModeDetector() val listener = Consumer { isDark -> println("dark mode: $isDark") } detector.registerListener(listener) // Later, stop receiving updates. detector.removeListener(listener) ``` The full interface: ```kotlin interface IDarkModeDetector { fun isDark(): Boolean fun registerListener(listener: Consumer) fun removeListener(listener: Consumer) } ``` ### Use a no-op detector `NoopDarkModeDetector` implements `IDarkModeDetector` without touching the OS. It always returns `false` from `isDark()` and ignores listeners — useful in tests or headless environments: ```kotlin import dev.nucleusframework.darkmodedetector.IDarkModeDetector import dev.nucleusframework.darkmodedetector.NoopDarkModeDetector val detector: IDarkModeDetector = NoopDarkModeDetector ``` ## What's next - [System colors](/docs/os/system-color) — read the system accent color and high-contrast state. - [System info](/docs/os/system-info) — query the OS, CPU, memory, and battery from Kotlin. - [OS integration](/docs/os) — the rest of the desktop OS APIs. --- # System accent colour URL: https://nucleusframework.dev/en/docs/os/system-color Read the user's OS accent colour and high-contrast setting as reactive Compose state. The `system-color` module reads the user's OS accent colour and high-contrast setting from Kotlin. Both are exposed as reactive Compose state that recomposes when the user changes them in system settings, so you can feed the accent straight into a Compose color scheme. Supported on macOS, Windows, and Linux. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.system-color:") } ``` ## Read the system accent colour Call `systemAccentColor()` inside a composable. It returns a `Color?` and recomposes whenever the user picks a different accent. Fall back to your own palette when it returns `null`: ```kotlin import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import dev.nucleusframework.systemcolor.systemAccentColor @Composable fun App() { val accent = systemAccentColor() val scheme = when { accent != null -> darkColorScheme(primary = accent) else -> darkColorScheme() } MaterialTheme(colorScheme = scheme) { // The UI recomposes when the accent colour changes. } } ``` Read the high-contrast setting the same way with `isSystemInHighContrast()`: ```kotlin import androidx.compose.runtime.Composable import dev.nucleusframework.systemcolor.isSystemInHighContrast @Composable fun AppContent() { val highContrast = isSystemInHighContrast() // Adjust borders, elevation, or contrast tokens when highContrast is true. } ``` ## How it works On each supported platform, a native JNI bridge starts observing the OS accent-colour and contrast change notifications and forwards them to registered listeners. `systemAccentColor()` and `isSystemInHighContrast()` subscribe to that bridge and hold the current value in Compose state. Any composable that reads them recomposes when the value changes, so there is no polling and no manual refresh. `systemAccentColor()` returns `null` when the platform cannot supply an accent colour. That includes any platform other than macOS, Windows, or Linux. Treat `null` as "use your own brand colour". Not every OS or configuration exposes an accent colour. Always handle the `null` case and fall back to your own palette. ## API reference ### Check platform support ```kotlin fun isSystemAccentColorSupported(): Boolean ``` Returns `true` when the current platform can report an accent colour. This is a plain function, not a composable, so you can call it outside composition — for example, to enable or disable a settings toggle. ### Read the accent colour ```kotlin @Composable fun systemAccentColor(): Color? ``` Returns the current accent colour, or `null` when the platform does not support it. Recomposes when the accent changes. ### Read the high-contrast flag ```kotlin @Composable fun isSystemInHighContrast(): Boolean ``` Returns `true` when the OS is in high-contrast mode. Recomposes when the user toggles the accessibility contrast setting. ## What's next - [Dark mode detection](/docs/os/dark-mode) — pair with the accent colour to derive a full OS-driven theme. - [System info](/docs/os/system-info) — read the OS, hardware, and battery details from Kotlin. - [OS integration](/docs/os) — the rest of the native platform modules. --- # System info URL: https://nucleusframework.dev/en/docs/os/system-info Read host details — OS, CPU, memory, disks, GPUs, batteries, network, processes, and users — through the SystemInfo object. `SystemInfo` lets you read host details from Kotlin: the operating system, CPU, memory, disks, GPUs, batteries, network interfaces, processes, and users. Each reading is a snapshot taken at call time, backed by a per-platform native library on macOS, Windows, and Linux. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.system-info:") } ``` ## Read the host `SystemInfo` is an `object`. Call its methods directly and check for `null`: a value that cannot be retrieved on a given host returns `null` rather than a guess. ```kotlin import dev.nucleusframework.systeminfo.SystemInfo fun main() { val os = SystemInfo.osInfo() ?: return println("OS: ${os.longOsVersion} (${os.cpuArch})") val mem = SystemInfo.memoryInfo() println("Memory: ${mem?.usedMemory?.div(1024 * 1024)} MB / ${mem?.totalMemory?.div(1024 * 1024)} MB") SystemInfo.gpus().forEach { gpu -> println("GPU: ${gpu.name} — ${gpu.dedicatedVideoMemory / 1024 / 1024} MB") } val battery = SystemInfo.batteryInfo() println("Battery: ${battery?.let { "${(it.stateOfCharge * 100).toInt()}% (${it.state})" } ?: "none"}") } ``` Call `SystemInfo.isAvailable()` first when you need to know whether the native library loaded on the current platform. ## How it works Each method delegates to a native library, one per operating system, loaded through JNI. The library reads the underlying platform APIs and returns plain Kotlin data classes. The API is snapshot-oriented: each call reads the current values and returns immediately. There is no polling or subscription. To display readings on a refresh interval, call the method again and cache the result yourself. Nullable returns and empty lists mean the value was unavailable, not that an error was thrown. List methods such as `disks()` and `gpus()` return an empty list when nothing could be read; single-value methods such as `cpuInfo()` return `null`. ## API reference ### Operating system ```kotlin val os = SystemInfo.osInfo() // name, osVersion, longOsVersion, kernelVersion, distributionId, // hostName, cpuArch, uptime, bootTime ``` `osInfo()` returns `OsInfo?`. ### CPU ```kotlin val cpu = SystemInfo.cpuInfo() // CpuGlobalInfo? cpu?.cpus?.forEach { core -> println("${core.brand}: ${core.frequency} MHz, usage ${core.cpuUsage}%") } println("Global usage: ${cpu?.globalCpuUsage}%") ``` `cpuInfo()` returns a `CpuGlobalInfo?` holding `globalCpuUsage`, `physicalCoreCount`, and a `cpus: List` with one entry per logical core. ### Memory and disks ```kotlin SystemInfo.memoryInfo() // MemoryInfo? — total, free, available, used, and swap counters SystemInfo.disks() // List — name, fileSystem, mountPoint, totalSpace, availableSpace ``` ### GPUs, batteries, and network ```kotlin SystemInfo.gpus() // List SystemInfo.batteryInfo() // BatteryInfo? — stateOfCharge, state, cycleCount, health, … SystemInfo.networks() // List SystemInfo.connectivityInfo() // ConnectivityInfo? — isConnected, meteredStatus ``` `BatteryInfo.state` is a `BatteryState` enum: `Charging`, `Discharging`, `Full`, or `Unknown`. `ConnectivityInfo.meteredStatus` is a `MeteredStatus` enum: `NOT_AVAILABLE`, `UNKNOWN`, `UNMETERED`, or `METERED`. ### Processes and users ```kotlin SystemInfo.processes() // List — every running process SystemInfo.process(pid) // ProcessInfo? — a single process by pid: Long SystemInfo.users() // List ``` ### Other readings ```kotlin SystemInfo.components() // List — temperature sensors SystemInfo.motherboard() // MotherboardInfo? SystemInfo.product() // ProductInfo? SystemInfo.idleTime() // Long — seconds since last user input, or -1 if unavailable ``` ## Notes - Some readings are platform-specific. Temperature sensors through `components()` are sparse on Windows, and `motherboard()` is most complete on Linux. - `processes()` walks every process and can be expensive on a busy host. Use `process(pid)` when you only need one. `idleTime()` returns `-1` when the platform library did not load, the same value `isAvailable()` reports as `false`. ## What's next - [System color](/docs/os/system-color) — read the system accent and high-contrast state. - [Dark mode](/docs/os/dark-mode) — detect and observe the system light/dark setting. - [Modules](/docs/concepts/modules) — the full list of Nucleus modules and their artifacts. --- # macOS menu bar URL: https://nucleusframework.dev/en/docs/os/menu-macos Build the native macOS menu bar from Compose, with items, keyboard shortcuts, icons, badges, and submenus that react to state. `menu-macos` lets you build the native macOS menu bar — the strip at the top of the screen, not inside your window — from Compose. You declare menus and items as a tree, and the native `NSMenu` is rebuilt whenever the state it reads changes. This module only does anything on macOS. `NativeMenuBar` is a no-op on other platforms, so you can leave the call in shared code. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.menu-macos:") } ``` This pulls in the [`sf-symbols`](https://developer.apple.com/sf-symbols/) constants transitively, so you can reference symbols by name without misspelling them. ## Build a menu bar Call `NativeMenuBar` from a composable near your application root. The first `Menu` call becomes the application menu (the bold entry named after your app). ```kotlin import androidx.compose.runtime.Composable import dev.nucleusframework.menu.macos.* import dev.nucleusframework.sfsymbols.SFSymbolObjectsAndTools import kotlin.system.exitProcess @Composable fun App() { NativeMenuBar { Menu("MyApp") { Item("About MyApp") { showAbout() } Separator() Item("Quit", shortcut = NativeKeyShortcut("q")) { exitProcess(0) } } Menu("File") { Item( "New", shortcut = NativeKeyShortcut("n"), icon = NsMenuItemImage.SystemSymbol(SFSymbolObjectsAndTools.DOCUMENT_BADGE_PLUS), ) { newDocument() } Item( "Open…", shortcut = NativeKeyShortcut("o"), icon = NsMenuItemImage.SystemSymbol(SFSymbolObjectsAndTools.FOLDER), ) { open() } } MenuHelp("Help") { Item("Documentation") { openDocs() } } } // … window content } ``` ## How it works `NativeMenuBar` saves the current menu bar on first composition, then installs a new `NSMenu` built from your DSL. The builder lambda is a normal composable body: it runs during composition, and the native menu is rebuilt whenever a state read inside it changes. When the composable leaves the composition, the saved menu bar is restored. Because the menu is tied to the composition, host `NativeMenuBar` from your application root rather than from a window's content. That way the menu survives when windows open and close. Menus declared with `MenuWindow` and `MenuHelp` are handed to AppKit as the well-known Window and Help menus. macOS then populates them itself — the window list and "Bring All to Front" for Window, and a search field at the top of Help. ## API reference ### Add menus and submenus `Menu` adds a top-level menu at the bar level, or a submenu inside another menu. Submenus nest to any depth. ```kotlin NativeMenuBar { Menu("Edit") { Item("Undo", shortcut = NativeKeyShortcut("z")) { undo() } Item("Redo", shortcut = NativeKeyShortcut("z", shift = true)) { redo() } Separator() Menu("Find") { Item("Find…", shortcut = NativeKeyShortcut("f")) { find() } Item("Find Next", shortcut = NativeKeyShortcut("g")) { findNext() } } } } ``` Use `MenuWindow` and `MenuHelp` for the standard Window and Help menus. Their content is optional — macOS fills them in. ### Add items `Item` adds a regular menu item. The trailing lambda is the click callback, dispatched on the Swing event dispatch thread. Common parameters: `enabled`, `shortcut`, `icon`, `state`, `badge`, `subtitle`, `toolTip`, and `tag`. ```kotlin Item( "Export…", shortcut = NativeKeyShortcut("e", shift = true), subtitle = "Save a copy as PDF", enabled = document != null, ) { export() } ``` ### Add checkboxes and radio buttons `CheckboxItem` and `RadioButtonItem` map their boolean state to the item's checkmark for you. Manage the selection yourself. ```kotlin var darkMode by remember { mutableStateOf(false) } NativeMenuBar { Menu("View") { CheckboxItem( "Dark mode", checked = darkMode, onCheckedChange = { darkMode = it }, ) Separator() RadioButtonItem("Light", selected = !darkMode, onClick = { darkMode = false }) RadioButtonItem("Dark", selected = darkMode, onClick = { darkMode = true }) } } ``` If you need finer control, `Item` accepts a `state` of `NsMenuItemState.OFF`, `NsMenuItemState.ON`, or `NsMenuItemState.MIXED` directly. ### Add separators, section headers, and search ```kotlin Menu("History") { SearchField(placeholder = "Search history") SectionHeader("Today") Item("nucleusframework.dev") { open(url) } Separator() SectionHeader("Earlier") Item("developer.apple.com") { open(url) } } ``` `SearchField` filters the menu's other items by title as you type. `SectionHeader` requires macOS 14+ and falls back to a disabled item on older systems. ### Set keyboard shortcuts `NativeKeyShortcut` defaults to the Command (⌘) modifier. Add `shift`, `option`, `control`, or `function` as needed, and use `NativeKey` constants for non-printable keys. ```kotlin Item("Save", shortcut = NativeKeyShortcut("s")) { save() } Item("Save As…", shortcut = NativeKeyShortcut("s", shift = true)) { saveAs() } Item("Cancel", shortcut = NativeKeyShortcut(NativeKey.ESCAPE, command = false)) { cancel() } ``` ### Add icons `NsMenuItemImage` sources an item's image from an SF Symbol, a named AppKit image, or a file path. ```kotlin Item("Settings", icon = NsMenuItemImage.SystemSymbol(SFSymbolGeneral.GEAR)) { openSettings() } ``` The `sf-symbols` module exposes each symbol category — `SFSymbolGeneral`, `SFSymbolObjectsAndTools`, `SFSymbolPower`, and more — as typed constants. `NsMenuItemImage.SystemSymbol` also accepts a raw symbol name string. ### Add badges `NsMenuItemBadge` shows a system-styled badge to the right of an item (macOS 14+). Use `Count` for a plain number, `Text` for a custom string, or `Alerts`, `NewItems`, and `Updates` for system-localized labels. ```kotlin Item("Inbox", badge = NsMenuItemBadge.Count(unreadCount)) { openInbox() } Item("Software Update", badge = NsMenuItemBadge.Updates(pendingUpdates)) { openUpdates() } ``` ## What's next - [System tray](/docs/os/system-tray) — a menu-bar status item and its context menu. - [Dark mode](/docs/os/dark-mode) — detect the system appearance to drive a checkable menu item. - [Freedesktop icons](/docs/os/freedesktop-icons) — the equivalent named-icon set for Linux. --- # Spell check URL: https://nucleusframework.dev/en/docs/os/spell-check Check spelling in Compose text fields through each operating system's native engine — Hunspell on Linux, NSSpellChecker on macOS, and the Windows Spell Checking API. The `spellcheck` module checks spelling from Kotlin through the spell-check engine each operating system already ships. It exposes a Compose wrapper that adds suggestions to a text field's context menu, and an engine API you can call without Compose. ## Add the dependency `nucleus-application` exposes `spellcheck` as an `api` dependency, so the Compose integration is already on the classpath of any Nucleus app: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.nucleus-application:") } ``` Add the module explicitly only when you want the engine without the Compose layer: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.spellcheck:") } ``` ## Check a text field Wrap the field in `SpellcheckContextMenu`. Right-clicking a misspelled word lists suggestions and an "Add to dictionary" entry above or below the standard Cut/Copy/Paste items: ```kotlin import androidx.compose.foundation.text.BasicTextField import dev.nucleusframework.application.spellcheck.SpellcheckContextMenu @Composable fun NoteField(value: String, onValueChange: (String) -> Unit) { SpellcheckContextMenu(text = value, onTextChange = onValueChange) { BasicTextField(value = value, onValueChange = onValueChange, singleLine = true) } } ``` Picking a suggestion replaces the clicked word and calls `onTextChange` with the new text. There is a second overload for the state-based text fields, which manages the text itself: ```kotlin import androidx.compose.foundation.text.input.rememberTextFieldState import dev.nucleusframework.application.spellcheck.SpellcheckContextMenu val state = rememberTextFieldState("helo world") SpellcheckContextMenu(state = state) { TextField(state = state) } ``` ## Choose the check language Without arguments, the session uses `Locale.getDefault()`. Pass `locale` to check one field in a different language: ```kotlin SpellcheckContextMenu( text = value, onTextChange = onValueChange, locale = Locale.FRENCH, ) { BasicTextField(value = value, onValueChange = onValueChange) } ``` To switch the whole process, assign `SpellChecker.locale`. Loading a dictionary touches the disk, so warm the session off the main thread: ```kotlin import dev.nucleusframework.spellcheck.SpellChecker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext LaunchedEffect(locale) { SpellChecker.locale = locale withContext(Dispatchers.IO) { SpellChecker.ensureSession(locale) } } ``` Assigning `locale` drops the current session; the next access builds one for the new language. When you pass both `session` and `locale` to the composable, `session` wins. ## Place the menu items `menuPlacement` decides whether suggestions land above or below the editing commands: ```kotlin import dev.nucleusframework.application.spellcheck.SpellcheckMenuPlacement SpellcheckContextMenu( text = value, onTextChange = onValueChange, menuPlacement = SpellcheckMenuPlacement.Top, ) { BasicTextField(value = value, onValueChange = onValueChange) } ``` | Value | Order | |---|---| | `Bottom` (default) | Cut/Copy/Paste, separator, suggestions | | `Top` | Suggestions, separator, Cut/Copy/Paste | ## How it works Each platform has a spell-check engine, reached over JNI. macOS uses `NSSpellChecker`, Windows uses the Spell Checking API (`ISpellChecker`), and Linux loads Hunspell with `dlopen` — so there is no compile-time dependency on a distro package. A `SpellcheckSession` owns one loaded dictionary for one locale. `SpellChecker` keeps a process-wide session and rebuilds it when you change `locale`. Words you add through "Add to dictionary" are appended to `~/.config/nucleus/spellcheck/.dic` (honoring `XDG_CONFIG_HOME`); the OS-level learned-word lists on macOS and Windows are left untouched. When no engine is available — an unsupported platform, a missing Hunspell library, or no matching dictionary — every operation is a no-op instead of an error: `check` and `addToDictionary` return `false`, `suggest` returns an empty list, and `SpellcheckContextMenu` renders its content unchanged. **Linux** Hunspell is loaded at run time from the first soname that resolves: `libhunspell-1.7.so.0`, `libhunspell-1.7.so`, `libhunspell.so.0`, `libhunspell-1.6.so.0`, then `libhunspell.so`. Dictionaries are `.aff` + `.dic` pairs, searched in `$DICPATH` (colon-separated) first, then `/usr/share/hunspell`, `/usr/share/myspell/dicts`, `/usr/share/myspell`, `/usr/local/share/hunspell`, and `$XDG_DATA_HOME/hunspell` (default `~/.local/share/hunspell`). The tag is resolved as `lang_COUNTRY`, then language-only, then through aliases — `en` tries `en_US`, `en_GB`, `en`; `fr` tries `fr_FR`. End users need their distro's `hunspell` runtime library and a dictionary package. **macOS** `NSSpellChecker` provides the engine and the language list. No dictionary files are needed, and the requested locale is matched against the languages macOS reports as available. **Windows** The Windows Spell Checking API (`ISpellChecker`) provides the engine. No dictionary files are needed; languages come from the installed Windows language packs. ## API reference ### Compose integration Package `dev.nucleusframework.application.spellcheck`, shipped in `nucleus-application`. | Symbol | Description | |---|---| | `SpellcheckContextMenu(text, onTextChange, session?, locale?, menuPlacement, content)` | Adds suggestions to the context menu of the field in `content`. | | `SpellcheckContextMenu(state, session?, locale?, menuPlacement, content)` | Same, for a `TextFieldState`-based field. | | `SpellcheckMenuPlacement` | `Top`, `Bottom`. | | `NucleusSpellcheckInstaller.menuItems(word, session, onSuggestion, onAddToDictionary, separator?, menuPlacement)` | Builds the suggestion items to merge into a menu you assemble yourself. | | `drawMisspellingSquiggles(drawScope, layout, ranges, color)` | Draws underlines for the given ranges in a custom text renderer. | ### Engine Package `dev.nucleusframework.spellcheck`, shipped in `spellcheck`. | Symbol | Description | |---|---| | `SpellChecker` | Process-wide singleton: `locale`, `session`, `sessionIfReady`, `isAvailable`, `ensureSession()`, `check()`, `suggest()`, `addToDictionary()`, `misspellings()`. | | `SpellcheckSession(locale, osName, dictionaryDirectories, userDictionaryFile)` | One loaded dictionary. `AutoCloseable`; exposes `locale`, `isAvailable`, `dictionaryTag`, `check`, `suggest`, `addToDictionary`, `misspellings`. | | `SpellcheckSession.defaultUserDictionaryFile(locale)` | Path of the user dictionary for a locale. | | `DictionaryLocator.defaultDirectories()` / `find(locale, directories)` | Where dictionaries are searched, and the pair that matches a locale. | | `DictionaryFiles(aff, dic, tag)` | A resolved dictionary pair. | | `SpellcheckWord(start, end, word)` | A token and its range in the text. | ### Text helpers | Function | Description | |---|---| | `iterateWords(text)` | Every token in the text. | | `misspellingRanges(text) { isCorrect }` | Ranges rejected by the predicate. | | `wordAt(text, offset)` | The token containing an offset, or `null`. | | `replaceWord(text, word, replacement)` | Text with one token replaced. | | `buildSpellcheckMenuModel(word, session, range?, maxSuggestions)` | Suggestions plus the localized "Add to dictionary" label. | | `applySuggestion(text, model, suggestion)` | Text with the model's range replaced. | `SpellcheckMenuModel.localizedAddToDictionaryLabel(locale)` translates the label; the fallback is `DEFAULT_ADD_TO_DICTIONARY_LABEL`. ## Notes - Tokens are Unicode letters with internal apostrophes. Hyphens split words, and all-digit tokens or tokens longer than 64 characters are skipped. - `SpellChecker.session` blocks on first access while the dictionary loads. Use `sessionIfReady` on a frame path, or warm it with `ensureSession()` from a background dispatcher. - Under Jewel, `JewelDecoratedWindow` and `JewelDecoratedDialog` install `ProvideJewelSpellcheckMenu` so the items match Jewel's menu chrome. ## What's next - [Context menus](/docs/window/context-menu) — the menu chrome the suggestions appear in. - [Native access](/docs/performance/native-access) — how Nucleus binds to native libraries. - [Ecosystem](/docs/ecosystem) — JVM libraries that cover the rest of your app. --- # SF Symbols URL: https://nucleusframework.dev/en/docs/os/sf-symbols Type-safe Kotlin constants for Apple SF Symbol names used by macOS APIs. `sf-symbols` provides Apple's [SF Symbols](https://developer.apple.com/sf-symbols/) names as typed Kotlin constants. macOS APIs that draw a symbol take its name as a string; instead of writing `"scissors"` by hand, you reference `SFSymbolObjectsAndTools.SCISSORS` and let the compiler check it. Other Nucleus macOS modules accept these constants wherever they take an icon. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.sf-symbols:") } ``` `menu-macos` exposes `sf-symbols` through `api()`. If you already depend on `menu-macos`, the constants are on your classpath without a separate declaration. ## Reference a symbol Every symbol is a value of the sealed interface `SFSymbol`. Pick a constant from the category enum that matches the symbol's context: ```kotlin import dev.nucleusframework.sfsymbols.SFSymbolGeneral import dev.nucleusframework.sfsymbols.SFSymbolMedia import dev.nucleusframework.sfsymbols.SFSymbolObjectsAndTools val gear = SFSymbolGeneral.GEAR val play = SFSymbolMedia.PLAY val scissors = SFSymbolObjectsAndTools.SCISSORS ``` Each constant carries its SF Symbols name in `symbolName`. Pass the constant straight to a macOS API that takes a symbol — for example, a native menu item icon: ```kotlin import dev.nucleusframework.menu.macos.NsMenuItemImage import dev.nucleusframework.sfsymbols.SFSymbolObjectsAndTools val icon = NsMenuItemImage.SystemSymbol(SFSymbolObjectsAndTools.SCISSORS) ``` For a name not covered by the constants, or to target a symbol from a newer SF Symbols release, use `SFSymbol.Custom`: ```kotlin import dev.nucleusframework.sfsymbols.SFSymbol val custom = SFSymbol.Custom("my.custom.symbol") ``` ## How it works Every constant exposes a `symbolName` string that holds its SF Symbols name — `GEAR` resolves to `"gear"`, `PLAY` to `"play"`. `Custom` wraps an arbitrary string in the same type, so any `SFSymbol` is ultimately a name that macOS can look up with `NSImage.imageWithSystemSymbolName`. When you hand an `SFSymbol` to a macOS module, only the `symbolName` reaches the native layer. The system resolves that name to the installed SF Symbols glyph at render time, so the symbol follows the user's system appearance without shipping any image assets. The constants are plain strings, so the module compiles on any platform. SF Symbols themselves require macOS 11 or later; on earlier macOS or other operating systems, a name resolves to nothing. ## API reference ### Categories `SFSymbol` is a sealed interface. Each category is an enum whose entries map to SF Symbols names; `Custom` is a value class for everything else. All 21 category enums implement `SFSymbol`, so any constant is usable wherever an `SFSymbol` is expected. | Category | Symbols | Examples | | --- | --- | --- | | `SFSymbolGeneral` | 1362 | `GEAR`, `GEARSHAPE` | | `SFSymbolHuman` | 596 | `ACCESSIBILITY`, `BRAIN` | | `SFSymbolShapes` | 542 | `APP`, `APP_BADGE` | | `SFSymbolArrows` | 535 | `ARROW_LEFT`, `ARROW_CLOCKWISE` | | `SFSymbolTransportation` | 417 | `AIRPLANE`, `AIRPLANE_ARRIVAL` | | `SFSymbolObjectsAndTools` | 358 | `SCISSORS`, `FOLDER`, `TRASH` | | `SFSymbolDevices` | 349 | `DESKTOPCOMPUTER`, `KEYBOARD` | | `SFSymbolNumbers` | 301 | `_0_CIRCLE`, `_0_CIRCLE_FILL` | | `SFSymbolWeather` | 286 | `CLOUD`, `SUN_MAX`, `BOLT` | | `SFSymbolStatus` | 273 | `CHECKMARK`, `INFO_CIRCLE` | | `SFSymbolCommerce` | 264 | `BAG`, `BAG_BADGE_PLUS` | | `SFSymbolMedia` | 238 | `PLAY`, `PAUSE`, `STOP` | | `SFSymbolLetters` | 142 | `A_CIRCLE`, `A_SQUARE` | | `SFSymbolTextFormatting` | 117 | `BOLD`, `CHARACTER` | | `SFSymbolCommunication` | 101 | `ENVELOPE`, `PHONE`, `MESSAGE` | | `SFSymbolPower` | 76 | `POWER`, `POWER_CIRCLE` | | `SFSymbolSecurity` | 62 | `KEY`, `KEY_FILL` | | `SFSymbolHealth` | 57 | `ALLERGENS`, `BASEBALL` | | `SFSymbolLocation` | 47 | `FLAG`, `COMPASS_DRAWING` | | `SFSymbolTime` | 45 | `ALARM`, `ALARM_FILL` | | `SFSymbolConnectivity` | 27 | `CABLE_COAXIAL`, `CABLE_CONNECTOR` | Number symbols begin with a digit, which is not a valid Kotlin identifier, so their constants are prefixed with an underscore: `SFSymbolNumbers._0_CIRCLE` maps to `"0.circle"`. ### Build a custom symbol `SFSymbol.Custom(symbolName: String)` is an inline value class. Its `symbolName` is used verbatim, so use it for symbols outside the predefined constants or from a newer SF Symbols version: ```kotlin SFSymbol.Custom("bird.fill") SFSymbol.Custom("my.custom.symbol") ``` ### Use a symbol with menu-macos `NsMenuItemImage.SystemSymbol` has an overload that takes an `SFSymbol` directly, so a constant becomes a menu item icon without touching its `symbolName`: ```kotlin import dev.nucleusframework.menu.macos.NsMenuItemImage import dev.nucleusframework.sfsymbols.SFSymbolGeneral NsMenuItemImage.SystemSymbol(SFSymbolGeneral.GEAR) ``` ## What's next - [Native menu bar](/docs/os/menu-macos) — attach `SystemSymbol` icons to menu items. - [FreeDesktop icon names](/docs/os/freedesktop-icons) — the equivalent typed icon constants for Linux. - [OS integration](/docs/os) — the rest of the desktop OS modules. --- # FreeDesktop icon names URL: https://nucleusframework.dev/en/docs/os/freedesktop-icons Typed Kotlin constants for the names defined by the FreeDesktop Icon Naming Specification. `freedesktop-icons` provides the standard icon names from the [FreeDesktop Icon Naming Specification](https://specifications.freedesktop.org/icon-naming/latest/) as typed Kotlin constants. Linux icon themes resolve icons by name: instead of shipping `info.png`, you reference `dialog-information` and the active theme renders its own version. Other Nucleus Linux modules accept these constants wherever they take an icon. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.freedesktop-icons:") } ``` `notification-linux` and `launcher-linux` expose `freedesktop-icons` through `api()`. If you already depend on either, the constants are on your classpath without a separate declaration. ## Reference an icon Each icon is a value of the sealed interface `FreedesktopIcon`. Pick a constant from the category enum that matches the icon's context: ```kotlin import dev.nucleusframework.freedesktop.icons.FreedesktopIcon val info = FreedesktopIcon.Status.DIALOG_INFORMATION val open = FreedesktopIcon.Action.DOCUMENT_OPEN val printer = FreedesktopIcon.Device.PRINTER ``` For names outside the spec, a file path, or a `file://` URI, use `FreedesktopIcon.Custom`: ```kotlin val named = FreedesktopIcon.Custom("my-app-icon") // theme lookup by name val path = FreedesktopIcon.Custom("/home/user/icon.png") // absolute path val uri = FreedesktopIcon.Custom("file:///home/user/icon.png") ``` `FreedesktopIcon.flag` builds a country flag name from an ISO 3166-1 alpha-2 code: ```kotlin val flag = FreedesktopIcon.flag("fr") // FreedesktopIcon.Custom("flag-fr") ``` ## How it works Every constant carries a `value` string that holds its spec name — `DIALOG_INFORMATION` resolves to `"dialog-information"`. `Custom` and `flag` wrap arbitrary strings in the same type, so any `FreedesktopIcon` is ultimately a name that a theme can look up. When you hand a `FreedesktopIcon` to a Linux module — notifications, launcher quicklists — the `value` is sent over D-Bus or written into a `.desktop` file as plain text. The system's active icon theme turns that name into a concrete image at render time, so the app follows the user's theme without shipping any bitmaps. The constants are plain strings, so they compile on any platform. Off Linux they resolve to nothing visible, since no FreeDesktop theme is present. ## API reference ### Categories `FreedesktopIcon` is a sealed interface. Each category is an enum whose entries map to spec names; `Custom` is a value class for everything else. | Category | Examples | | --- | --- | | `FreedesktopIcon.Action` | `DOCUMENT_OPEN`, `EDIT_COPY`, `MEDIA_PLAYBACK_START` | | `FreedesktopIcon.Animation` | `PROCESS_WORKING` | | `FreedesktopIcon.Application` | `ACCESSORIES_TEXT_EDITOR`, `MULTIMEDIA_VOLUME_CONTROL`, `UTILITIES_TERMINAL` | | `FreedesktopIcon.Category` | `APPLICATIONS_INTERNET`, `PREFERENCES_DESKTOP`, `SYSTEM_HELP` | | `FreedesktopIcon.Device` | `PRINTER`, `CAMERA_PHOTO`, `INPUT_KEYBOARD` | | `FreedesktopIcon.Emblem` | `IMPORTANT`, `FAVORITE`, `SHARED` | | `FreedesktopIcon.Emote` | `FACE_SMILE`, `FACE_SAD`, `FACE_WINK` | | `FreedesktopIcon.MimeType` | `TEXT_X_GENERIC`, `IMAGE_X_GENERIC`, `X_OFFICE_DOCUMENT` | | `FreedesktopIcon.Place` | `FOLDER`, `USER_HOME`, `NETWORK_SERVER` | | `FreedesktopIcon.Status` | `DIALOG_INFORMATION`, `DIALOG_WARNING`, `BATTERY_LOW` | | `FreedesktopIcon.Custom` | value class for raw names, file paths, or `file://` URIs | ### Build a custom icon `FreedesktopIcon.Custom(value: String)` is an inline value class. Its `value` is used verbatim, whether it is a theme name, an absolute path, or a `file://` URI. ### Build a country flag `FreedesktopIcon.flag(countryCode: String)` lowercases the code and returns `Custom("flag-")`: ```kotlin FreedesktopIcon.flag("us") // Custom("flag-us") FreedesktopIcon.flag("jp") // Custom("flag-jp") ``` ## Notes - Not every theme implements every icon name. Sticking to the canonical FreeDesktop set gives the highest chance of a match. - Flag icons are not part of the FreeDesktop spec and depend on the theme shipping `flag-` names. ## What's next - [Linux notifications](/docs/os/notification-linux) — pass a `FreedesktopIcon` as the notification icon. - [Linux launcher](/docs/lifecycle/launcher-linux) — reuse the same constants in `.desktop` actions. - [System tray](/docs/os/system-tray) — set a per-theme tray icon by name. --- # App lifecycle, from cold launch to sleep URL: https://nucleusframework.dev/en/docs/lifecycle The lifecycle modules connect a Nucleus app to the OS events and services that outlive any single window. The lifecycle modules connect a Nucleus app to the operating-system events and services that outlive any single window: cold launch and instance locking, deep links, dock and taskbar integration, launch at login, background scheduling, and energy management. Each stage is a separate module, and you add only the ones you need. ## Modules by lifecycle stage Each stage maps to a module and a page. The two foundations, `core-runtime` and `nucleus-application`, back several of the rows below. | Stage | Module | Page | |---|---|---| | Cold start and instance locking | `core-runtime` (`SingleInstanceManager`) | [Single instance](/docs/lifecycle/single-instance) | | URL and file associations | `core-runtime` (`DeepLinkHandler`) + plugin DSL | [Deep links](/docs/lifecycle/deep-links) | | Bundle id, version, and vendor at runtime | `core-runtime` (`NucleusApp`) | [App metadata](/docs/lifecycle/app-metadata) | | How the app was packaged | `core-runtime` (`ExecutableRuntime`) | [Executable type](/docs/lifecycle/executable-type) | | Dock menu (macOS) | `launcher-macos` | [Launcher: macOS](/docs/lifecycle/launcher-macos) | | Taskbar overlays and jump list (Windows) | `launcher-windows` | [Launcher: Windows](/docs/lifecycle/launcher-windows) | | Unity launcher and quicklist (Linux) | `launcher-linux` | [Launcher: Linux](/docs/lifecycle/launcher-linux) | | Launch at login | `autolaunch` | [Auto-launch](/docs/lifecycle/auto-launch) | | Periodic and cron background work | `scheduler` | [Scheduler](/docs/lifecycle/scheduler) | | `SMAppService` agents (macOS) | `service-management-macos` | [Service management](/docs/lifecycle/service-management) | | Energy efficiency and keep-awake | `energy-manager` | [Energy manager](/docs/lifecycle/energy-manager) | | Progress in the dock or taskbar | `taskbar-progress` | [Taskbar progress](/docs/lifecycle/taskbar-progress) | ## How it works Each module wraps the native API for its stage — `SMAppService`, `ITaskbarList3`, `NSDockTile`, `com.canonical.Unity.LauncherEntry`, systemd timers, and others — behind a Kotlin surface, so you keep a single codebase while the app still integrates with each OS through its own mechanisms. The modules are independent: add only the ones you need, and each degrades to a no-op on platforms it does not support. Two modules underpin the rest. `core-runtime` provides the runtime primitives: `Platform`, `NucleusApp`, `SingleInstanceManager`, `DeepLinkHandler`, and `ExecutableRuntime`. `nucleus-application` provides the `nucleusApplication { }` entry point, which starts the runtime and initializes those primitives. Read those two pages first, then the module that matches the behavior you need. Deep link schemes are declared in the Gradle build with the `protocol(name, schemes...)` DSL, which the plugin translates into macOS Apple Events, Windows registry entries, and Linux `.desktop` MIME associations at package time. ## What's next - [Single instance](/docs/lifecycle/single-instance) — enforce one running instance and forward launch arguments to it. - [Deep links](/docs/lifecycle/deep-links) — register custom URL schemes and handle incoming URLs. - [Architecture](/docs/concepts/architecture) — what `core-runtime` and `nucleus-application` set up under your `main` function. --- # One app, one process URL: https://nucleusframework.dev/en/docs/lifecycle/single-instance Enforce a single running instance of your app and forward a second launch's arguments to the primary process. `SingleInstanceManager` restricts your app to one running instance. When a second launch is detected, it forwards that invocation — CLI arguments or a deep-link URI — to the running process instead of starting a duplicate. It ships in `core-runtime` and uses the same mechanism on macOS, Windows, and Linux. ## Add the dependency `SingleInstanceManager` ships in `core-runtime`, which `nucleus-application` already depends on. Add it explicitly only when you use it without `nucleus-application`: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.core-runtime:") } ``` ## Enforce a single instance When you start your app with `nucleusApplication`, single-instance enforcement is on by default. The second launch exits immediately and the primary window returns to the foreground: ```kotlin import dev.nucleusframework.application.nucleusApplication fun main(args: Array) = nucleusApplication(args) { DecoratedWindow(onCloseRequest = ::exitApplication) { /* ... */ } } ``` `enableSingleInstance` defaults to `true`. When the app also handles deep links, the secondary instance's URI is forwarded to the primary through [`DeepLinkHandler`](/docs/lifecycle/deep-links) without extra wiring. ## Wire it manually When you drive Compose's `application { }` yourself, call `SingleInstanceManager.isSingleInstance` and exit when it returns `false`: ```kotlin import dev.nucleusframework.core.runtime.SingleInstanceManager fun main() = application { var restoreRequested by remember { mutableStateOf(false) } val isPrimary = remember { SingleInstanceManager.isSingleInstance( onRestoreFileCreated = { // Runs on the second instance. `this` is the restore-request Path; // write whatever you want the primary to read. }, onRestoreRequest = { // Runs on the primary instance when another launch is detected. restoreRequested = true }, ) } if (!isPrimary) { exitApplication() return@application } Window(onCloseRequest = ::exitApplication) { LaunchedEffect(restoreRequested) { if (restoreRequested) { window.toFront() window.requestFocus() restoreRequested = false } } } } ``` `onRestoreFileCreated` is optional. Omit it when the second launch has no payload to hand off. ## How it works `SingleInstanceManager` holds a lock file in `Configuration.lockFilesDir` (the system temp directory by default) and acquires it with `java.nio.channels.FileLock`. The lock is kernel-backed and released when the JVM exits or crashes, so a force-killed primary does not lock the app out. When the lock is already held, the second launch writes a restore-request file next to the lock. The primary watches that directory with a `WatchService` and calls `onRestoreRequest` when the file appears. This is one mechanism across all three platforms — there is no macOS LaunchServices, Windows named mutex, or Linux D-Bus name ownership to wire up per OS. The two callbacks form the IPC channel. `onRestoreFileCreated` runs on the second instance and receives the restore-request `Path` as its receiver, so it writes a payload — typically a deep-link URI via [`DeepLinkHandler.writeUriTo`](/docs/lifecycle/deep-links). `onRestoreRequest` runs on the primary with the same `Path` and reads it back. ## API reference ### SingleInstanceManager | Member | Signature | Notes | |---|---|---| | `isSingleInstance` | `(onRestoreFileCreated: (Path.() -> Unit)? = null, onRestoreRequest: Path.() -> Unit): Boolean` | Returns `true` on the primary, `false` when another instance holds the lock. `onRestoreFileCreated` runs on the secondary; `onRestoreRequest` runs on the primary. | | `configuration` | `var Configuration` | Assign it before the first `isSingleInstance` call; changing it afterwards throws `IllegalStateException`. | ### Configuration `Configuration` is a `data class` with two constructor parameters. The file names and paths are derived from them and are read-only. | Property | Type / default | Description | |---|---|---| | `lockFilesDir` | `Path` = `java.io.tmpdir` | Directory holding the lock and restore-request files. | | `lockIdentifier` | `String` = `AppIdProvider.appId()` | Base name used to derive the file names. Change it to isolate the lock. | | `lockFileName` | `String` (derived) | `"$lockIdentifier.lock"`. | | `restoreRequestFileName` | `String` (derived) | `"$lockIdentifier.restore_request"`. | | `lockFilePath` / `restoreRequestFilePath` | `Path` (derived) | The two file names resolved under `lockFilesDir`. | ## Notes - Opt out with `nucleusApplication(args, enableSingleInstance = false)`. - The default `lockIdentifier` is the app ID resolved by `AppIdProvider.appId()` — the same ID the Gradle plugin injects via `nucleus.app.id`, so two apps never share a lock. - The lock file lives under the per-user temp directory, so two users on the same machine can each run the app. - If the lock file is stale or unreadable, `SingleInstanceManager` deletes it and retries once. If the retry finds the lock still held, the process is a secondary and sends a restore request. Only an unrecoverable error makes it fail open and continue as the primary. ## What's next - [Deep links](/docs/lifecycle/deep-links) — forward `myapp://...` URLs from a second launch to the primary. - [App metadata](/docs/lifecycle/app-metadata) — where the app ID that names the lock comes from. - [Lifecycle overview](/docs/lifecycle) — the rest of the runtime lifecycle APIs. --- # Deep links and URL schemes URL: https://nucleusframework.dev/en/docs/lifecycle/deep-links Register a custom URL scheme at packaging time and receive the incoming deep-link URI in your Nucleus app at runtime. Deep links let you register a custom `myapp://` URL scheme and receive the incoming URI in Kotlin. The Nucleus Gradle plugin registers the scheme with the OS at packaging time, and `DeepLinkHandler` in `core-runtime` delivers the URI at runtime, whether it arrives through macOS Apple Events or command-line arguments. ## Add the dependency `DeepLinkHandler` ships in `core-runtime`, which `nucleus-application` pulls in transitively. If you already depend on `nucleus-application`, you have it. Otherwise add the module directly: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.core-runtime:") } ``` ## Declare the URL scheme Register the scheme in the `nativeDistributions` block of the Nucleus plugin. The plugin writes the platform registration into each installer it builds: ```kotlin title="build.gradle.kts" nucleus.application { nativeDistributions { // protocol(name, vararg schemes) // name is shown in the OS UI; schemes are the URL prefixes (without "://"). protocol("MyApp", "myapp") } } ``` ## Handle inbound URIs Inside `nucleusApplication { }`, register a callback with `onDeepLink`. It selects the delivery path for the active backend (AWT or Tao) and fires for both the cold-start URI and later activations: ```kotlin import java.net.URI fun main(args: Array) = nucleusApplication(args) { onDeepLink { uri: URI -> // Route on uri.host / uri.path / query parameters. navigateTo(uri) } DecoratedWindow(onCloseRequest = ::exitApplication) { // ... } } ``` Without the `nucleusApplication` entry point, wire `DeepLinkHandler` yourself. `setHandler` is safe on every backend; on macOS you also install the AWT Apple Events handler: ```kotlin import dev.nucleusframework.core.runtime.DeepLinkHandler fun main(args: Array) { DeepLinkHandler.setHandler(args) { uri -> navigateTo(uri) } // macOS only, and only for AWT-driven launches. DeepLinkHandler.installAwtAppleEventHandler() application { // ... } } ``` `installAwtAppleEventHandler` initializes AWT. On the Tao backend (`decorated-window-tao`), AWT and Tao each create their own `NSApp` and deadlock the event loop on macOS. Use `nucleusApplication { onDeepLink { … } }`, which picks the right path per backend. ## How it works Each OS delivers URLs differently: | OS | Mechanism | |---|---| | macOS | Apple Events. The running app receives `GetURL` events, exposed through `Desktop.setOpenURIHandler` on the AWT backend. The plugin registers the scheme in `Info.plist` under `CFBundleURLTypes`. | | Windows | A registry entry under `HKCU\Software\Classes\`. Opening a `myapp://...` URL launches your `.exe` with the URL as the first command-line argument — a new process. | | Linux | A `.desktop` file with `MimeType=x-scheme-handler/myapp;` plus an `xdg-mime` association. Activation passes the URL as a command-line argument. | `setHandler(args, onDeepLink)` reconciles the difference. On the AWT path, `installAwtAppleEventHandler` registers an Apple Events handler so the callback fires while the app is already running; on Windows and Linux, `setHandler` scans `args` for the first deep-link-shaped entry. From 2.2 it accepts both authority forms (`myapp://path`) and opaque scheme-only forms such as `myapp:?query=…` (common from `xdg-open` on Linux). Windows drive paths (`C:\…`, `D:/…`) are rejected so they never look like schemes. The cold-start URI is delivered to the handler once per process, so registering from a Compose scope that re-runs on recomposition does not re-fire it. Windows and Linux start a new process for each activation. Combine with `SingleInstanceManager` to forward the URI to the already-running instance: the secondary process records its URI with `captureFromArgs` and writes it with `writeUriTo(path)` before exiting, and the primary reads it back with `readUriFrom(path)`. The plugin generates every static registration — `Info.plist` entries, Windows registry keys, and `.desktop` files. You declare the scheme once. ## API reference ### Gradle DSL ```kotlin nativeDistributions { protocol(name: String, vararg schemes: String) } ``` ### DeepLinkHandler `object DeepLinkHandler` in `dev.nucleusframework.core.runtime`. | Member | Signature | Notes | |---|---|---| | `setHandler` | `(args: Array, onDeepLink: (URI) -> Unit)` | Registers the callback and delivers the cold-start URI once. Safe on every backend. | | `captureFromArgs` | `(args: Array)` | Records a URI from `args` into `uri` without installing a callback or touching AWT. Used by the single-instance secondary bootstrap. | | `installAwtAppleEventHandler` | `()` | macOS only. Installs the AWT Apple Events handler and initializes AWT. | | `writeUriTo` | `(path: Path)` | Writes the current `uri` to a file. Pair with `SingleInstanceManager`. | | `readUriFrom` | `(path: Path)` | Reads a URI from a file and fires the callback. | | `deliver` | `(newUri: URI)` | Injects a URI into the handler chain. Used by alternative delivery paths and tests. | | `uri` | `URI?` (read-only) | The most recently received URI, including the cold-start one. | ### NucleusApplicationScope.onDeepLink ```kotlin nucleusApplication { onDeepLink { uri -> /* ... */ } } ``` Registers a `DeepLinkHandler` callback that defers until the application is running and selects the backend-appropriate delivery path. ## Notes - `DeepLinkHandler.register(args, onDeepLink)` is deprecated: it always installs the AWT handler and is incompatible with the Tao backend on macOS. Use `onDeepLink { … }` or `setHandler`. - Test in development with `xdg-open myapp://test` (Linux), `open myapp://test` (macOS), or `start myapp://test` (Windows). Registration through the installer only applies to packaged builds — see [Launcher: Windows](/docs/lifecycle/launcher-windows). - The cold-start URI is also stored in `DeepLinkHandler.uri`; read it after your handler is registered. - For file associations (opening a `.csv` with your app), use the plugin's `fileAssociation(...)` DSL instead. ## What's next - [Single instance](/docs/lifecycle/single-instance) — forward a deep link from a relaunched process to the running one. - [Launcher: macOS](/docs/lifecycle/launcher-macos) — the `Info.plist` and Apple Events integration. - [Launcher: Windows](/docs/lifecycle/launcher-windows) — registry registration and jump-list entries. --- # App metadata at runtime URL: https://nucleusframework.dev/en/docs/lifecycle/app-metadata Read your app's id, version, vendor, display name, and Windows AUMID from Kotlin at runtime. `NucleusApp` lets you read your application's metadata — id, version, vendor, display name, and Windows AUMID — from Kotlin at runtime. The values come from what you configure in the Nucleus Gradle plugin and are injected at build time, so you don't hardcode them or read them off disk yourself. ## Add the dependency `NucleusApp` lives in the `core-runtime` module, which the Nucleus Gradle plugin already pulls in. Add the coordinate explicitly only when you read metadata from a module that does not depend on it: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.core-runtime:") } ``` ## Read the metadata `NucleusApp` is an object. Read its properties directly: ```kotlin import androidx.compose.foundation.layout.Column import androidx.compose.material.Text import androidx.compose.runtime.Composable import dev.nucleusframework.core.runtime.NucleusApp @Composable fun AboutDialog() { Column { Text(NucleusApp.appName ?: NucleusApp.appId) NucleusApp.version?.let { Text("Version $it") } NucleusApp.vendor?.let { Text("by $it") } } } ``` Each property resolves once and is cached, so reading it repeatedly is cheap. Use `isConfigured` to tell whether the Nucleus plugin injected metadata at all: ```kotlin if (NucleusApp.isConfigured) { // Running as a packaged app initAutoUpdater() } ``` `isConfigured` is `true` under `./gradlew run` too, because the plugin injects `nucleus.app.id` into the `run` task. To gate auto-update, telemetry, or anything else you don't want firing in development, test `ExecutableRuntime.isDev()` instead. ## How it works The Gradle plugin delivers metadata to `NucleusApp` through two channels. Each property resolves from them in order, and the first non-blank value wins: 1. **System properties** during `./gradlew run` — the plugin injects `-Dnucleus.app.id` and `-Dnucleus.executable.type`. `NucleusApp` reads the other `nucleus.app.*` properties only if you set them yourself. 2. **A classpath resource** in packaged builds — `nucleus/nucleus-app.properties`, baked into the application jar. For `appId` only, a third step applies: when neither source provides a value, `appId` derives from the main class name through `AppIdProvider`, falling back to `NucleusApp` if that too is missing. A single block in `build.gradle.kts` therefore flows into every runtime module without manual wiring. The same values back the single-instance lock identifier, the Windows AUMID used for toast notifications and badge counts, and the MSIX startup task id — all read through `NucleusApp`. ## API reference ### NucleusApp An object exposing the injected metadata as read-only properties. | Property | Type | Value | |---|---|---| | `appId` | `String` | Application id. Matches the plugin's `packageName`. Falls back to the main class name. | | `appName` | `String?` | Display name, or `null` if not configured. | | `version` | `String?` | Application version, or `null` if not configured. | | `vendor` | `String?` | Application vendor, or `null` if not configured. | | `description` | `String?` | Application description, or `null` if not configured. | | `aumid` | `String` | Windows Application User Model ID. Falls back to `appId` when not set. | | `startupTaskId` | `String?` | MSIX startup task id, injected when `appx { addAutoLaunchExtension = true }`. Used by [Auto-launch](/docs/lifecycle/auto-launch). `null` otherwise. | | `isConfigured` | `Boolean` | `true` when the plugin injected metadata through the `nucleus.app.id` system property or the classpath resource. | ## Notes - `appId` and `aumid` are non-null; every other value is nullable. Guard nullable reads or supply your own fallback. - `isConfigured` checks the `nucleus.app.id` system property and the classpath resource, not the other individual properties. ## What's next - [Auto-launch](/docs/lifecycle/auto-launch) — start your app at login using `startupTaskId`. - [Executable type](/docs/lifecycle/executable-type) — distinguish a DMG, Flatpak, or `./gradlew run` at runtime. - [Single instance](/docs/lifecycle/single-instance) — the single-instance lock keyed on your app id. --- # How was I launched? URL: https://nucleusframework.dev/en/docs/lifecycle/executable-type Detect at runtime how the app was packaged and launched, from a DMG or Snap to a plain ./gradlew run. `ExecutableRuntime` reports how the current process was packaged and launched: a DMG, an APPX, a Flatpak, a Snap, an AppImage, an `.exe` installer, or a plain `./gradlew run`. Read it when your code needs to branch on the packaging format, for example to pick an auto-update channel, resolve a writable path, or detect a sandbox. ## Add the dependency `ExecutableRuntime` and `ExecutableType` ship in `core-runtime`, which the Nucleus umbrella already pulls in. Add it explicitly only when you depend on the module on its own: ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.core-runtime:") } ``` The `aot-runtime` module re-exports both types as type aliases, so AOT-only code can depend on it alone. ## Detect the packaging format Call `ExecutableRuntime.type()` and branch on the returned `ExecutableType`: ```kotlin import dev.nucleusframework.core.runtime.ExecutableRuntime import dev.nucleusframework.core.runtime.ExecutableType when (ExecutableRuntime.type()) { ExecutableType.APPX -> showMicrosoftStoreUpdate() ExecutableType.SNAP -> showSnapStoreUpdate() ExecutableType.FLATPAK -> showFlathubUpdate() ExecutableType.PKG -> showMacAppStoreUpdate() else -> checkForUpdatesDirectly() } ``` For a single format, use the matching `is*()` check: ```kotlin if (ExecutableRuntime.isAppX()) { // Sandboxed: write to per-package state, not arbitrary HOME paths } ``` ## How it works The Nucleus Gradle plugin records the packaging format at build time. For packaged JVM apps it sets `-Dnucleus.executable.type=` in the launcher's `.cfg` file; for GraalVM native-image builds, where that system property is unavailable, it writes a `.nucleus-executable-type` marker file next to the executable. `ExecutableRuntime.type()` reads the system property first and falls back to the marker file. Detection matches your packaging pipeline exactly, and does not rely on filesystem heuristics such as checking whether `/snap/` is in the path. When neither source is present, as with `./gradlew run` or any unpackaged run, `type()` returns `ExecutableType.DEV`. Treat that as your dev-mode signal. Features that need accurate packaging detection short-circuit in dev mode to avoid false positives. For example, [`AutoLaunch.wasStartedAtLogin(args)`](/docs/lifecycle/auto-launch) returns `false` whenever `ExecutableRuntime.isDev()` is `true`. ## API reference ### ExecutableRuntime | Member | Returns | Notes | |---|---|---| | `type()` | `ExecutableType` | Reads the `nucleus.executable.type` property, then the marker file. | | `type(propertyName)` | `ExecutableType` | Reads the format from a custom system property. | | `parseType(rawValue)` | `ExecutableType` | Parses a raw string; unknown input maps to `DEV`. | | `markerVersion()` | `String?` | App version from the marker file (native-image builds). | | `isGraalVmNativeImage` | `Boolean` | `true` when running as a GraalVM native image. | | `isExe()`, `isMsi()`, `isNsis()`, `isNsisWeb()`, `isPortable()`, `isAppX()`, `isDmg()`, `isPkg()`, `isDeb()`, `isRpm()`, `isSnap()`, `isFlatpak()`, `isAppImage()`, `isPacman()`, `isZip()`, `isTar()`, `isSevenZ()`, `isDev()` | `Boolean` | One check per `ExecutableType` value. | ### ExecutableType An enum with one value per supported packaging format: - Windows: `EXE`, `MSI`, `NSIS`, `NSIS_WEB`, `PORTABLE`, `APPX` - macOS: `DMG`, `PKG` - Linux: `DEB`, `RPM`, `SNAP`, `FLATPAK`, `APPIMAGE`, `PACMAN` - Archives: `ZIP`, `TAR`, `SEVEN_Z` - Dev: `DEV` ## Notes - `DEV` covers `./gradlew run` and any other unpackaged run. It is also the fallback for an unknown or missing type, so a `DEV` result does not distinguish "in the IDE" from "misconfigured packaging". - `ExecutableRuntime` only knows the packaging format, not the app contents. For the running version, use [`NucleusApp.version`](/docs/lifecycle/app-metadata). ## What's next - [App metadata](/docs/lifecycle/app-metadata) — read the app id, version, and vendor at runtime. - [Auto-launch](/docs/lifecycle/auto-launch) — start the app at login, gated on the packaging format. - [Auto-update](/docs/packaging/auto-update) — choose an update strategy per distribution channel. --- # Launcher (macOS) URL: https://nucleusframework.dev/en/docs/lifecycle/launcher-macos Populate the Dock right-click menu of a macOS app with items, submenus, and separators, and handle clicks from Kotlin. `launcher-macos` lets you populate the Dock right-click menu of a macOS app from Kotlin. You define the menu entries — including submenus, separators, and disabled items — and receive a callback on the Swing EDT when the user clicks one. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.launcher-macos:") } ``` ## Populate the Dock menu Call `MacOsDockMenu.setDockMenu(...)` with the items to display, then set a `listener` to handle clicks. Each item carries a numeric `id` that the listener receives. ```kotlin import dev.nucleusframework.launcher.macos.DockMenuItem import dev.nucleusframework.launcher.macos.DockMenuListener import dev.nucleusframework.launcher.macos.MacOsDockMenu MacOsDockMenu.setDockMenu(listOf( DockMenuItem(id = 1, title = "New Window"), DockMenuItem(id = 2, title = "Open File..."), DockMenuItem.separator(id = 3), DockMenuItem( id = 4, title = "Recent", children = listOf( DockMenuItem(id = 41, title = "project.kt"), DockMenuItem(id = 42, title = "build.gradle.kts"), ), ), DockMenuItem(id = 5, title = "Preferences"), )) MacOsDockMenu.listener = DockMenuListener { id -> when (id) { 1 -> openNewWindow() 2 -> openFile() in 40..49 -> openRecent(id) 5 -> showPreferences() } } ``` Call `MacOsDockMenu.clearDockMenu()` to remove the menu and restore the default Dock behavior. All methods are no-ops off macOS and when the native library fails to load. Check `MacOsDockMenu.isAvailable` before you build the menu. ## How it works The first call to `setDockMenu(...)` installs a method swizzle on the running `NSApplicationDelegate` to intercept `applicationDockMenu:`. From then on, AppKit asks Nucleus for the menu whenever the user right-clicks (or Ctrl-clicks) the Dock tile. Click callbacks are delivered on the Swing EDT, so you can mutate Compose state directly from the listener without dispatching to another thread. macOS strips images from Dock menu items. Only text, separators, submenus, and the enabled/disabled state are rendered — the Dock process owns the drawing, not your app. For dragging files onto the Dock icon, AWT already exposes `Desktop.setOpenFileHandler` (and `Desktop.setOpenURIHandler` for deep links). `launcher-macos` does not duplicate that. ## API reference ### `MacOsDockMenu` The entry point. A single object with one global listener. | Member | Notes | |---|---| | `isAvailable: Boolean` | `false` off macOS or when the native library failed to load. | | `listener: DockMenuListener?` | One global listener; dispatch by `id` inside it. | | `setDockMenu(items: List)` | Replaces the menu. Installs the swizzle on the first call. | | `clearDockMenu()` | Removes the menu; the OS default returns. | ### `DockMenuItem` A `data class` describing one entry. | Property | Default | Notes | |---|---|---| | `id: Int` | required, `> 0` | Identifier passed to the listener. | | `title: String` | `""` | Display text. | | `enabled: Boolean` | `true` | Whether the item is clickable. | | `children: List` | `emptyList()` | A non-empty list makes the item a submenu. | `DockMenuItem.separator(id)` builds a separator entry. ### `DockMenuListener` A `fun interface` with a single method, `onItemClicked(itemId: Int)`, invoked on the Swing EDT. ## Notes - Test from a packaged build or `runDistributable`. The `java` launcher icon shown by `./gradlew run` uses no bundle identifier and will not display the menu meaningfully. - ProGuard / GraalVM: keep `dev.nucleusframework.launcher.macos.NativeMacOsDockMenuBridge` reachable. The module ships the metadata. ## What's next - [Taskbar progress](/docs/lifecycle/taskbar-progress) — Dock badge, bounce, and progress state. - [Deep links](/docs/lifecycle/deep-links) — handle `Desktop.setOpenURIHandler` URL events. - [Launcher (Windows)](/docs/lifecycle/launcher-windows) — taskbar jump lists and thumbnail toolbars. --- # Launcher (Windows) URL: https://nucleusframework.dev/en/docs/lifecycle/launcher-windows Drive the Windows taskbar from Kotlin with overlay icons, jump lists, thumbnail toolbar buttons, and badges. The `launcher-windows` module exposes four parts of the Windows shell from Kotlin: overlay icons on the taskbar button, jump lists in its right-click menu, buttons in the taskbar thumbnail preview, and numeric or glyph badges. Each part is a separate `object` you call directly; use only the ones you need. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.launcher-windows:") } ``` ## Set an overlay icon An overlay icon is a 16x16 status marker in the bottom-right corner of the taskbar button. Pass the AWT `Window` and an icon source: ```kotlin import dev.nucleusframework.launcher.windows.StockIcon import dev.nucleusframework.launcher.windows.TaskbarIconSource import dev.nucleusframework.launcher.windows.WindowsOverlayIcon WindowsOverlayIcon.setIcon( window, TaskbarIconSource.FromStock(StockIcon.WARNING), description = "1 unread message", ) // later WindowsOverlayIcon.clearIcon(window) ``` Overlay icons work with every packaging type. Each call returns `true` on success; check `WindowsOverlayIcon.isAvailable` first if you support non-Windows platforms. ## Build a jump list A jump list groups items in the taskbar right-click menu. `setJumpList` replaces the whole list atomically: ```kotlin import dev.nucleusframework.launcher.windows.JumpListCategory import dev.nucleusframework.launcher.windows.JumpListItem import dev.nucleusframework.launcher.windows.WindowsJumpListManager WindowsJumpListManager.setJumpList( categories = listOf( JumpListCategory("Recent", items = listOf( JumpListItem("Project A", arguments = "myapp://open?project=a"), JumpListItem("Project B", arguments = "myapp://open?project=b"), )), ), tasks = listOf( JumpListItem("New window", arguments = "myapp://new"), JumpListItem.SEPARATOR, JumpListItem("Settings", arguments = "myapp://settings"), ), ) ``` For unpackaged apps (NSIS, MSI, distributable), call `WindowsJumpListManager.setProcessAppId()` once at the very start of `main()`, before any window is created. APPX/MSIX apps skip this — Windows uses the package identity. When the user clicks a jump list item, Windows launches a new process with the item's `arguments` on the command line. Pair it with [`SingleInstanceManager`](/docs/lifecycle/single-instance) to forward those arguments to the already-running instance. ## Add thumbnail toolbar buttons The thumbnail toolbar adds up to 7 clickable buttons to the window's taskbar thumbnail preview. Register them once per window with `setButtons`: ```kotlin import dev.nucleusframework.launcher.windows.TaskbarIconSource import dev.nucleusframework.launcher.windows.ThumbnailToolbarButton import dev.nucleusframework.launcher.windows.WindowsThumbnailToolbar WindowsThumbnailToolbar.setButtons( window, buttons = listOf( ThumbnailToolbarButton(id = 0, tooltip = "Previous", icon = TaskbarIconSource.FromFile("icons/prev.ico")), ThumbnailToolbarButton(id = 1, tooltip = "Play", icon = TaskbarIconSource.FromFile("icons/play.ico")), ThumbnailToolbarButton(id = 2, tooltip = "Next", icon = TaskbarIconSource.FromFile("icons/next.ico")), ), onClick = { buttonId -> handleTransport(buttonId) }, ) ``` Windows does not allow adding buttons after the first registration. To change a button's icon, tooltip, or state later, call `updateButtons(window, buttons)` with the same ids. Call `unregister(window)` when the window closes. ## Show a badge Badges show a numeric count or a status glyph on the taskbar button and Start tile. They rely on an Application User Model ID that Windows grants only to packaged apps, so the badge API works under APPX/MSIX only. Call `initialize()` once before setting a badge: ```kotlin import dev.nucleusframework.launcher.windows.BadgeGlyph import dev.nucleusframework.launcher.windows.WindowsBadgeManager WindowsBadgeManager.initialize() WindowsBadgeManager.setGlyph(BadgeGlyph.NEW_MESSAGE) // or a numeric count WindowsBadgeManager.setCount(5) // clear WindowsBadgeManager.clear() ``` `setCount` shows 1–99 as the number and 99+ above that; `setCount(0)` clears it. Call `uninitialize()` on shutdown. ## How it works Overlay icons and the thumbnail toolbar use the COM `ITaskbarList3` interface, keyed by the HWND of your `java.awt.Window`. COM is initialized lazily on the first call, and thumbnail button clicks are delivered on the AWT event dispatch thread. Jump lists use `ICustomDestinationList`. Windows owns the menu, so clicks never reach your running process — the OS starts a fresh executable with the item's `arguments` appended to the command line. That is why jump list actions carry data through `arguments` rather than a callback. Badges use WinRT and require an AUMID, which Windows only grants to packaged apps. `WindowsBadgeManager.isAvailable` only reports whether the native library loaded — it is `true` on any Windows box. `initialize()` is what fails outside APPX/MSIX, so check its return value. Under APPX/MSIX the AUMID is left empty and Windows uses the package identity; pass an explicit `aumid` to `initialize` to override it. ## API reference ### Overlay icon | Member | Description | |---|---| | `WindowsOverlayIcon.setIcon(window, icon, description = "")` | Sets the overlay icon. Returns `Boolean`. | | `WindowsOverlayIcon.clearIcon(window)` | Removes the overlay icon. Returns `Boolean`. | | `WindowsOverlayIcon.isAvailable` | Whether the native library is loaded on this platform. | | `WindowsOverlayIcon.lastError` | Message from the last failed call, or `null`. | ### Jump list | Member | Description | |---|---| | `WindowsJumpListManager.setProcessAppId(aumid = null)` | Sets the process AUMID; call at the start of `main()` for unpackaged apps. | | `WindowsJumpListManager.setJumpList(categories, tasks, knownCategories)` | Replaces the whole jump list. All parameters default to empty. | | `WindowsJumpListManager.clearJumpList()` | Removes all entries. | Supporting types: | Type | Use | |---|---| | `JumpListCategory(name, items)` | A named group of items. | | `JumpListItem(title, arguments, description, icon, isSeparator)` | A click-to-launch entry; all parameters have defaults. | | `JumpListItem.SEPARATOR` | A visual separator (user tasks only). | | `KnownCategory.FREQUENT` / `KnownCategory.RECENT` | Shell-managed lists populated from your file associations. | ### Thumbnail toolbar | Member | Description | |---|---| | `WindowsThumbnailToolbar.setButtons(window, buttons, onClick = null)` | Registers up to 7 buttons. Callable once per window. | | `WindowsThumbnailToolbar.updateButtons(window, buttons)` | Updates icon, tooltip, and flags of already-registered buttons. | | `WindowsThumbnailToolbar.unregister(window)` | Removes the click callback and restores the window procedure. | `ThumbnailToolbarButton(id, tooltip, icon, enabled, hidden, noBackground, dismissOnClick, nonInteractive)` — `id` must be 0–6, `tooltip` at most 259 characters. The `onClick` handler is a `ThumbBarClickListener` receiving the clicked button's `id`. ### Badge | Member | Description | |---|---| | `WindowsBadgeManager.initialize(aumid = null)` | Prepares the badge subsystem. Required before any badge call; APPX/MSIX only. | | `WindowsBadgeManager.setCount(count)` | Sets a numeric badge; `0` clears it. | | `WindowsBadgeManager.setGlyph(glyph)` | Sets a status glyph; `BadgeGlyph.NONE` clears it. | | `WindowsBadgeManager.clear()` | Clears the badge. | | `WindowsBadgeManager.uninitialize()` | Releases native resources. | `BadgeGlyph` values: `NONE`, `ACTIVITY`, `ALARM`, `ALERT`, `ATTENTION`, `AVAILABLE`, `AWAY`, `BUSY`, `ERROR`, `NEW_MESSAGE`, `PAUSED`, `PLAYING`, `UNAVAILABLE`. ### Icon sources Every API that takes an icon accepts a `TaskbarIconSource`: - `TaskbarIconSource.FromStock(stockIcon)` — one of 93 `StockIcon` values (`WARNING`, `ERROR`, `INFO`, `SHIELD`, `FOLDER`, ...). No files needed. - `TaskbarIconSource.FromFile(path)` — an `.ico` file on disk. - `TaskbarIconSource.FromResource(dllPath, index)` — an icon index inside a resource DLL such as `shell32.dll` or `imageres.dll`. ## Notes - Jump list items need a real executable. They do not fire under `./gradlew run`, where the process is `java.exe` — test from `runDistributable` or a packaged build. - For a progress bar inside the taskbar button itself, rather than an icon overlay, use [`taskbar-progress`](/docs/lifecycle/taskbar-progress). ## What's next - [Launcher (macOS)](/docs/lifecycle/launcher-macos) — the dock menu equivalent on macOS. - [Launcher (Linux)](/docs/lifecycle/launcher-linux) — launcher entries and quicklists on Linux. - [Taskbar progress](/docs/lifecycle/taskbar-progress) — progress bars in the taskbar button. - [Single instance](/docs/lifecycle/single-instance) — forward jump list arguments to a running app. --- # Launcher (Linux) URL: https://nucleusframework.dev/en/docs/lifecycle/launcher-linux Set count badges, progress bars, urgency flags, and right-click quicklist menus on the Linux launcher from Kotlin. `launcher-linux` maps the `com.canonical.Unity.LauncherEntry` D-Bus interface and the `com.canonical.dbusmenu` protocol to Kotlin. Use it to draw count badges, progress bars, and urgency flags on your launcher icon, and to attach a right-click quicklist menu. Desktop shells that implement the interface include Unity, KDE Plasma, Plank, and budgie-panel. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.launcher-linux:") } ``` ## Set badges and progress Build the launcher URI from your `.desktop` file id, then set individual properties or push them all in one signal: ```kotlin import dev.nucleusframework.launcher.linux.LauncherProperties import dev.nucleusframework.launcher.linux.LinuxLauncherEntry val appUri = LinuxLauncherEntry.appUri("myapp.desktop") // Badge count LinuxLauncherEntry.setCount(appUri, 42) // Progress bar (0.0..1.0) LinuxLauncherEntry.setProgress(appUri, 0.65) // Set several properties in one signal LinuxLauncherEntry.update( appUri, LauncherProperties( count = 5, countVisible = true, progress = 0.8, progressVisible = true, urgent = false, ), ) ``` `setCount` and `setProgress` are wrappers over `update`, so a single call that sets several properties emits one D-Bus signal. ## Add a quicklist menu A quicklist is the right-click menu on the launcher icon. Create a `LinuxQuicklist` on a D-Bus object path, set a click listener, install the menu, then point the launcher entry at the quicklist path: ```kotlin import dev.nucleusframework.freedesktop.icons.FreedesktopIcon import dev.nucleusframework.launcher.linux.DbusmenuItem import dev.nucleusframework.launcher.linux.LauncherProperties import dev.nucleusframework.launcher.linux.LinuxLauncherEntry import dev.nucleusframework.launcher.linux.LinuxQuicklist val quicklist = LinuxQuicklist("/com/example/MyApp/Menu") quicklist.listener = LinuxQuicklist.Listener { id -> when (id) { 1 -> openNewWindow() 2 -> openFile() 8 -> exitApp() } } quicklist.setMenu( listOf( DbusmenuItem(id = 1, label = "New Window", icon = FreedesktopIcon.Action.WINDOW_NEW), DbusmenuItem(id = 2, label = "Open File", icon = FreedesktopIcon.Action.DOCUMENT_OPEN), DbusmenuItem.separator(id = 3), DbusmenuItem( id = 8, label = "Quit", icon = FreedesktopIcon.Action.APPLICATION_EXIT, disposition = DbusmenuItem.Disposition.ALERT, ), ), ) LinuxLauncherEntry.update(appUri, LauncherProperties(quicklist = quicklist.objectPath)) ``` Icons use [`freedesktop-icons`](/docs/os/freedesktop-icons) for symbolic names. Clear the quicklist and release the D-Bus server on shutdown: ```kotlin LinuxLauncherEntry.update(appUri, LauncherProperties(quicklist = "")) quicklist.dispose() ``` ## How it works The Unity Launcher API is a D-Bus signal protocol. You emit an `Update` signal on `com.canonical.Unity.LauncherEntry` carrying your `.desktop` file URI and a dictionary of properties (count, progress, urgent, quicklist path). Desktop shells subscribe to the signal and render the changes on your launcher icon. Nucleus talks to D-Bus through GIO/GDBus over a single native bridge, without a Java D-Bus library. Each `LinuxQuicklist` registers its own `com.canonical.dbusmenu` object. The shell queries that object for the layout, and item clicks are dispatched back on the Swing event dispatch thread with `SwingUtilities.invokeLater`. The `.desktop` filename determines which launcher icon receives the signals. `launcher-linux` never derives it for you — always pass it explicitly, as in `LinuxLauncherEntry.appUri("myapp.desktop")`. (Auto-detection from the running process is a [taskbar-progress](/docs/lifecycle/taskbar-progress) feature.) ## API reference ### Update the launcher entry `LinuxLauncherEntry` is an object. | Member | Description | |---|---| | `isAvailable: Boolean` | `false` outside Linux or when the native library failed to load. | | `appUri(desktopFileId: String): String` | Builds the `application://` URI used as the signal subject. | | `update(appUri, properties: LauncherProperties): Boolean` | Emits one `Update` signal with every non-null field. | | `setCount` / `clearCount` / `setProgress` / `clearProgress` / `setUrgent` / `setUpdating` | Wrappers over `update`. | | `registerQueryHandler(appUri)` / `unregister()` | Register a `Query` handler for the entry state, then release it on shutdown. | ### Describe properties `LauncherProperties` has `count`, `countVisible`, `progress`, `progressVisible`, `urgent`, `quicklist` (the object path of the menu server, or an empty string to unset it), and `updating`. All fields are nullable, and null fields are skipped when the signal is emitted. ### Build a quicklist | Member | Description | |---|---| | `objectPath: String` | The D-Bus path you bound. Pass it to `LauncherProperties(quicklist = ...)`. | | `listener: Listener?` | Click callback; `Listener` is a `fun interface` with `onItemClicked(itemId: Int)`. | | `setMenu(items: List): Boolean` | Replaces the menu layout and registers the D-Bus object. | | `dispose()` | Tears down the D-Bus server. | ### Build a menu item `DbusmenuItem` has `id`, `label` (`_` marks a mnemonic), `icon` (a `FreedesktopIcon`), `enabled`, `visible`, `type` (`ItemType.STANDARD` or `ItemType.SEPARATOR`), `toggleType` (`ToggleType.NONE`, `CHECKBOX`, or `RADIO`), `toggleState` (`-1` indeterminate, `0` unchecked, `1` checked), `shortcut`, `disposition` (`Disposition.NORMAL`, `INFORMATIONAL`, `WARNING`, or `ALERT`), and `children`. `DbusmenuItem.separator(id)` builds a separator. ## Notes - The dbusmenu protocol is stateless on the server side; the shell re-queries the layout on every open. To change a toggle's check state, call `setMenu(...)` again with the new `toggleState`. - For cross-platform progress bars, see [`taskbar-progress`](/docs/lifecycle/taskbar-progress), which delegates to `launcher-linux` on Linux. - XFCE without the `docklike-plugin` doesn't implement the Unity API. Detection succeeds, but nothing is rendered. ## What's next - [Taskbar progress](/docs/lifecycle/taskbar-progress) — cross-platform progress and badges. - [Freedesktop icons](/docs/os/freedesktop-icons) — symbolic icon names for menu items. - [Launcher (macOS)](/docs/lifecycle/launcher-macos) — the Dock menu equivalent on macOS. --- # Launch at login URL: https://nucleusframework.dev/en/docs/lifecycle/auto-launch Enable, disable, and query start-at-login from Kotlin with a single API that resolves the backend for the current packaging format. The `autolaunch` module lets you enable, disable, and query start-at-login from Kotlin without writing per-OS code. It resolves the backend for the current packaging format at runtime — MSIX startup tasks, the Win32 registry, macOS `SMAppService`, Linux systemd user services, and the Flatpak background portal — and respects states the user set outside your app. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.autolaunch:") } ``` On macOS, the JNI bridge in `nucleus.service-management-macos` is pulled in transitively, so you do not declare it separately. ## Check and toggle auto-launch Read the current state with `AutoLaunch.state()` and branch on it. Call `enable()` or `disable()` to change it, and `openSystemSettings()` when the user must act in system UI: ```kotlin import dev.nucleusframework.autolaunch.AutoLaunch import dev.nucleusframework.autolaunch.AutoLaunchState when (AutoLaunch.state()) { AutoLaunchState.ENABLED -> {} AutoLaunchState.DISABLED -> AutoLaunch.enable() AutoLaunchState.DISABLED_BY_USER -> AutoLaunch.openSystemSettings() AutoLaunchState.UNSUPPORTED -> { /* macOS < 13, unsupported Linux environment */ } else -> {} } ``` Never loop on `BLOCKED_BY_USER`. When the user disables the app through Task Manager or System Settings, `enable()` returns `BLOCKED_BY_USER` and keeps refusing until the user re-enables it manually. Send them to `openSystemSettings()` instead. To wire a toggle in Compose, drive it from `state()`: ```kotlin Switch( checked = AutoLaunch.state() == AutoLaunchState.ENABLED, enabled = AutoLaunch.state() != AutoLaunchState.DISABLED_BY_USER, onCheckedChange = { on -> if (on) AutoLaunch.enable() else AutoLaunch.disable() }, ) ``` ## Detect a login launch `wasStartedAtLogin(args)` reports whether the current process was spawned by the login mechanism rather than started by hand. Check it at your `main()` entry point to skip a splash screen or start minimized: ```kotlin fun main(args: Array) { if (AutoLaunch.wasStartedAtLogin(args)) { startMinimized = true } // ... } ``` On macOS the login AppleEvent is delivered after `NSApplication.run()` starts, so the first call from `main()` usually returns `false`. The method caches positive results, so it is safe to poll it from a `LaunchedEffect` once the Compose loop is up. ## How it works The runtime selects a backend from the operating system and the detected executable type — no per-OS conditional code on your side: | Packaging | Backend | |---|---| | MSIX | WinRT `Windows.ApplicationModel.StartupTask`. The plugin injects the manifest extension when `appx { addAutoLaunchExtension = true }`. | | Win32 (MSI / NSIS) | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, reading `StartupApproved\Run` to detect a user disable. | | macOS DMG / PKG (13+) | `SMAppService.mainApp`. Appears under System Settings → General → Login Items. | | Linux (host: deb / rpm / AppImage) | systemd user service under `~/.config/systemd/user/`. | | Linux (Flatpak) | XDG Desktop Portal `org.freedesktop.portal.Background`. | Three invariants the module enforces, which you should not work around: 1. **User choices are final.** Once the user disables auto-launch through system UI, the state is `DISABLED_BY_USER` and `enable()` returns `BLOCKED_BY_USER` until the user re-enables it. 2. **Silent updates do not reset state.** The Win32 backend writes to `Run` only on `enable()` and touches `StartupApproved` only on `disable()`. 3. **MSIX requires the manifest extension.** Without `` in `Package.appxmanifest`, the API returns `UNSUPPORTED`. Set `addAutoLaunchExtension = true` in the plugin block and the plugin adds it. ## API reference ### AutoLaunch `AutoLaunch` is an object; all methods are static. | Method | Returns | Notes | |---|---|---| | `state()` | `AutoLaunchState` | Current auto-launch state. | | `enable()` | `AutoLaunchResult` | Requests auto-launch on. | | `disable()` | `AutoLaunchResult` | Requests auto-launch off. | | `openSystemSettings()` | `Boolean` | Opens `ms-settings:startupapps` (Windows) or System Settings → General → Login Items (macOS). `true` if the UI launched. | | `wasStartedAtLogin(args)` | `Boolean` | Whether this process was spawned by login. Returns `false` in dev mode. | | `isEnabled()` | `Boolean` | `true` for `ENABLED` or `ENABLED_BY_POLICY`. | | `isUserLocked()` | `Boolean` | `true` when the state is `DISABLED_BY_USER`. | | `diagnostic()` | `String` | Human-readable backend trace for support tickets. | | `preload()` | `Unit` | Eagerly resolves the backend and loads its JNI library. Call from a background thread to avoid a first-call stall on macOS. | ### AutoLaunchState `ENABLED`, `DISABLED`, `DISABLED_BY_USER`, `DISABLED_BY_POLICY`, `ENABLED_BY_POLICY`, `UNSUPPORTED`. `DISABLED_BY_POLICY` and `ENABLED_BY_POLICY` are read-only Group Policy states (MSIX only). ### AutoLaunchResult `OK`, `UNCHANGED`, `BLOCKED_BY_USER`, `BLOCKED_BY_POLICY`, `UNSUPPORTED`, `ERROR`. ### AutoLaunchConfig Override the defaults before the first `AutoLaunch` call: | Property | Affects | |---|---| | `taskId` | MSIX startup task id; must match ``. | | `executablePath` | Win32 `HKCU\...\Run` path. Resolved from the running process when `null`. | | `autostartArgument` | CLI marker appended to the registered command (default `--nucleus-autostart`). | | `registryValueName` | Win32 `HKCU\...\Run` value name. | | `backgroundReason` | Reason string shown in the Flatpak portal prompt. | ## Notes - On MSIX, toggling auto-launch programmatically does not refresh the Task Manager "Startup apps" tab live — Windows updates that view after the next login. The stored state is correct; only the display lags. - macOS login detection reads the `keyAELaunchedAsLogInItem` marker from the AppleEvent that `loginwindow` dispatches at login. Apple ships no public API for this. - The generated Linux unit is ordered after `gnome-session-x11-services-ready.target` as well as `graphical-session.target` (2.5.6). `graphical-session.target` alone is reached before `gsd-xsettings` publishes `Xft.dpi`, so a toolkit that samples the display scale once at startup froze at 1.0 and rendered a HiDPI app at half size when it was started at login. systemd ignores ordering on units it has not loaded, so the dependency is inert outside GNOME. - `wasStartedAtLogin()` returns `false` in dev mode (`ExecutableRuntime.isDev()`) to avoid false positives from environment variables inherited from the parent shell. Production builds must set the `nucleus.executable.type` system property or ship the `.nucleus-executable-type` marker file, or `ExecutableRuntime` defaults to `DEV` and the method always returns `false`. ## What's next - [Executable type](/docs/lifecycle/executable-type) — how the runtime detects the packaging format that drives backend selection. - [Service management](/docs/lifecycle/service-management) — register login items, agents, and daemons on macOS. - [Single instance](/docs/lifecycle/single-instance) — route login launches into an already-running process. - [Scheduler](/docs/lifecycle/scheduler) — run background work after a login launch. --- # Scheduler — cron jobs that survive reboots URL: https://nucleusframework.dev/en/docs/lifecycle/scheduler Run periodic, calendar, and on-boot background tasks from Kotlin that the OS fires on schedule even when your app is closed. The scheduler module lets you run background tasks on a schedule from Kotlin, even when your app is closed. You register a task once and the OS — launchd on macOS, Task Scheduler on Windows, systemd user timers on Linux — fires it on time and handles wakeups and persistence. Tasks come in three shapes: periodic, calendar, and on-boot. `DesktopTaskScheduler` is not supported in sandboxed Mac App Store builds (`.pkg`), because it writes launchd plists at runtime, which the sandbox forbids. Use [Service Management](/docs/lifecycle/service-management) there instead. ## Add the dependency ```kotlin title="build.gradle.kts" plugins { kotlin("plugin.serialization") version "" } dependencies { implementation("dev.nucleusframework:nucleus.scheduler:") } ``` `kotlinx-serialization-json` is exposed transitively, so you can annotate task input with `@Serializable` without adding it yourself. ## Schedule a task ### 1. Define a task and a registry Implement `DesktopTask`, give it a `TaskId`, and register a factory for it. The OS relaunches your binary to run a task, so the registry is how the receiver maps an ID back to your code. ```kotlin import dev.nucleusframework.scheduler.DesktopTask import dev.nucleusframework.scheduler.TaskContext import dev.nucleusframework.scheduler.TaskId import dev.nucleusframework.scheduler.TaskRegistry import dev.nucleusframework.scheduler.TaskResult val SyncId = TaskId("sync") class SyncTask : DesktopTask { override suspend fun doWork(context: TaskContext): TaskResult { performSync() return TaskResult.Success } } val registry = TaskRegistry.Builder() .register(SyncId) { SyncTask() } .build() ``` ### 2. Handle scheduler invocations in main() When the OS fires a task, it relaunches your binary with a scheduler flag. Check for that at the top of `main()` and hand control to `DesktopBootReceiver`, which runs the task and exits. ```kotlin import dev.nucleusframework.scheduler.DesktopBootReceiver fun main(args: Array) { if (DesktopBootReceiver.isSchedulerInvocation(args)) { DesktopBootReceiver.handle(args = args, registry = registry) return } // Normal UI startup nucleusApplication(args) { /* ... */ } } ``` This check must come before any UI is built. Otherwise every scheduler fire opens a window. ### 3. Enqueue a request Build a `TaskRequest` with one of the three factories and enqueue it. Enqueuing is idempotent by default — see [`ExistingTaskPolicy`](#taskrequest) to change that. ```kotlin import dev.nucleusframework.scheduler.CronExpression import dev.nucleusframework.scheduler.DesktopTaskScheduler import dev.nucleusframework.scheduler.TaskRequest import java.time.LocalTime import kotlin.time.Duration.Companion.hours val scheduler = DesktopTaskScheduler.getInstance() // Every hour, from enqueue time scheduler.enqueue(TaskRequest.periodic(SyncId, 1.hours)) // Every day at 09:00, wall-clock scheduler.enqueue(TaskRequest.calendar(ReportId, CronExpression.everyDayAt(LocalTime.of(9, 0)))) // At system/user login scheduler.enqueue(TaskRequest.onBoot(StartupCheckId)) ``` ## How it works The OS schedulers can't run a Kotlin function — they only run binaries. So the scheduler registers your application binary with the platform's timer/job system and passes `--nucleus-scheduler-run ` on the command line. When that fires, your `main()` runs, `DesktopBootReceiver.isSchedulerInvocation(args)` returns `true`, and the receiver: 1. Loads the persisted `TaskContext` (input data and attempt count) from the per-task metadata file. 2. Checks any registered constraints against current system state via [`system-info`](/docs/os/system-info). 3. If they are satisfied, calls your `doWork(context)`. 4. Records the outcome — `Success`, `Failure`, `Retry`, or `ConstraintsNotMet` — and, on `Retry`, schedules a single re-run about 5 minutes later. | Platform | Mechanism | Plist / unit location | |---|---|---| | macOS | launchd plists | `~/Library/LaunchAgents/dev.nucleusframework...plist` | | Linux | systemd user service + timer via D-Bus to `org.freedesktop.systemd1.Manager` | `~/.config/systemd/user/nucleus--.{service,timer}` | | Windows | Task Scheduler 2.0 COM API (`ITaskService`) | `\Nucleus\\` | On Linux and Windows, the scheduler doesn't register the app binary directly — it writes a small wrapper (`.sh` / `.vbs`) that checks the binary still exists. If the user uninstalls the app without cleaning up, the wrapper self-destructs: it unregisters the timer/task, deletes its metadata, and removes itself. macOS doesn't get this trick, because the launchd entry must point directly at the binary to show up correctly in System Settings. Orphan cleanup there relies on you calling `DesktopTaskScheduler.cancelAll()` from your sign-out or reset flows. ## API reference ### DesktopTaskScheduler Obtain the singleton with `DesktopTaskScheduler.getInstance()`, or call the methods on the object directly. | Method | Returns | Description | |---|---|---| | `isAvailable()` | `Boolean` | Whether the platform backend is wired up. | | `enqueue(request)` | `Boolean` | Registers a task; `true` if scheduled or already present. | | `cancel(taskId)` | `Boolean` | Removes a task; `true` if it was found. | | `cancelAll()` | `Unit` | Removes every task this app registered. | | `isScheduled(taskId)` | `Boolean` | Whether the task is currently scheduled. | | `getTaskInfo(taskId)` | `TaskInfo?` | State, last/next run, run count, and last result. | | `getAllTasks()` | `List` | Info for every task this app registered. | ### TaskRequest Create requests with the factories `periodic(taskId, interval)`, `calendar(taskId, expression)`, and `onBoot(taskId)`. Each takes an optional builder lambda: ```kotlin import dev.nucleusframework.scheduler.ExistingTaskPolicy import dev.nucleusframework.scheduler.NetworkType import dev.nucleusframework.scheduler.RetryPolicy import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes TaskRequest.periodic(SyncId, 1.hours) { inputData(SyncInput(endpoint = "https://api.example.com")) retryPolicy(RetryPolicy.ExponentialBackoff(initialDelay = 30.minutes, maxAttempts = 3)) runImmediately() constraints { requiredNetworkType = NetworkType.UNMETERED requiresBatteryNotLow = true } existingTaskPolicy(ExistingTaskPolicy.REPLACE) } ``` The minimum periodic interval is 15 minutes; a shorter value throws. | Builder method | Description | |---|---| | `inputData(value)` | Attach a `@Serializable` payload, persisted as JSON. | | `retryPolicy(policy)` | `RetryPolicy.Linear` or `RetryPolicy.ExponentialBackoff`. Recorded on the request; the platform backends currently retry at a fixed delay regardless of the policy. | | `runImmediately()` | Run once on registration, in addition to the interval (periodic only). | | `constraints { ... }` | Network, charging, idle, and storage conditions. | | `existingTaskPolicy(policy)` | `KEEP` (default), `UPDATE_DATA`, or `REPLACE`. | ### CronExpression `everyDayAt(time)`, `everyWeekdayAt(time)` (Monday–Friday), `everyWeekdayAt(day, time)`, and `everyHour()`. Times use `java.time.LocalTime`; days use `java.time.DayOfWeek`. Expressions map to systemd `OnCalendar=` syntax on all platforms. ### TaskResult Return one of these from `doWork`: - `TaskResult.Success` - `TaskResult.Failure(message)` — permanent, no retry. - `TaskResult.Retry(message)` — schedules one re-run about 5 minutes later. The configured `RetryPolicy` is not applied by the platform backends yet. ### Constraints Set on the `constraints { }` block; all are checked at execution time. | Property | Type | Description | |---|---|---| | `requiredNetworkType` | `NetworkType` | `NOT_REQUIRED`, `CONNECTED`, or `UNMETERED`. | | `requiresBatteryNotLow` | `Boolean` | Requires battery above 15 %. | | `requiresCharging` | `Boolean` | Requires the device to be plugged in. | | `requiresDeviceIdle` | `Boolean` | Requires no user input for at least 5 minutes. | | `minimumStorageBytes` | `Long?` | Minimum free disk space on the app partition. | An unsatisfied periodic fire is silently skipped and re-checked on the next trigger. An unsatisfied calendar or on-boot fire bumps the attempt counter and schedules a single re-run about 5 minutes later. ### SchedulerConfig Every backend invokes the current executable, resolved from `ProcessHandle.current().info().command()`. If your app boots through a custom launcher, point the scheduler at that launcher instead: ```kotlin import dev.nucleusframework.scheduler.SchedulerConfig SchedulerConfig.executablePath = "/opt/myapp/myapp-launcher" SchedulerConfig.executableArguments = listOf("--background") ``` The resulting command is ` --nucleus-scheduler-run `. | Property | Type | Default | Behavior | |---|---|---|---| | `executablePath` | `String?` | `null` | Absolute path to the program the OS scheduler invokes. `null` or blank resolves the current executable. | | `executableArguments` | `List` | `emptyList()` | Arguments inserted before the `--nucleus-scheduler-run` flag. | `executablePath` must be absolute: the generated wrapper scripts check that the file still exists and unregister the task when it is gone, for example after an uninstall. Set both properties before the first `enqueue` call. The values are baked into the generated plist, unit, or task at enqueue time, so a later change only affects tasks scheduled afterwards. ## Notes - Don't put secrets in `inputData` — it's persisted as plain JSON in the per-task metadata file. Store references there and resolve credentials from the keychain at run time. - `CronExpression.everyHour()` means the wall-clock top of the hour, not "one hour from enqueue". For a fixed cadence relative to enqueue time, use `TaskRequest.periodic(id, 1.hours)`. - For tests, the `scheduler-testing` module ships `TestTaskRunner` for isolated unit tests and `TestDesktopTaskScheduler` with virtual time, execution history, and a `TestConstraintChecker`. ## What's next - [Service Management](/docs/lifecycle/service-management) — background agents and daemons for sandboxed macOS builds. - [System info](/docs/os/system-info) — the signals the scheduler reads to evaluate constraints. - [Auto-launch](/docs/lifecycle/auto-launch) — start your app itself at login, not just a task. --- # Scheduler testing URL: https://nucleusframework.dev/en/docs/lifecycle/scheduler-testing Test doubles for the scheduler that run background tasks in memory, with virtual time and controllable constraints. The scheduler-testing module provides test doubles for the [scheduler](/docs/lifecycle/scheduler) so you can exercise background tasks in unit and integration tests without touching the OS scheduler. It runs your `DesktopTask` implementations in memory, advances a virtual clock to fire periodic schedules, and lets you simulate constraint failures. ## Add the dependency Add the artifact on the test classpath. It brings in the `scheduler` module transitively. ```kotlin title="build.gradle.kts" dependencies { testImplementation("dev.nucleusframework:nucleus.scheduler-testing:") } ``` ## Test a task in isolation To unit-test a single task, call `TestTaskRunner.runTask()`. It invokes your task's `doWork()` with a fabricated `TaskContext` and returns the `TaskResult`, with no scheduler or OS involved. ```kotlin import dev.nucleusframework.scheduler.TaskData import dev.nucleusframework.scheduler.TaskId import dev.nucleusframework.scheduler.TaskResult import dev.nucleusframework.scheduler.testing.TestTaskRunner import kotlin.test.Test import kotlin.test.assertEquals class SyncTaskTest { @Test fun `sync succeeds`() = runTest { val result = TestTaskRunner.runTask( task = SyncTask(), taskId = TaskId("sync"), inputData = TaskData.of(SyncInput(endpoint = "https://test.api")), ) assertEquals(TaskResult.Success, result) } } ``` `taskId`, `inputData`, and `runAttemptCount` all have defaults, so a bare `TestTaskRunner.runTask(task = SyncTask())` works when your `doWork()` reads none of them. Set `runAttemptCount` to exercise retry-aware logic. ## How it works `TestDesktopTaskScheduler` implements the same internal `PlatformScheduler` interface that the real backend uses. Calling `install()` routes every `DesktopTaskScheduler` call — `enqueue()`, `cancel()`, `getTaskInfo()`, and the rest — to the in-memory instance instead of launchd, Task Scheduler, or systemd. `uninstall()` (also called by `close()`) restores the platform default. The test scheduler holds its own virtual clock. Enqueuing records the task and its enqueue time; `advanceTimeBy()` walks the clock forward and fires each periodic task at every interval boundary crossed, in chronological order. Each firing runs `doWork()` through the same code path as `runTask()` and appends an `ExecutionRecord` to the task's history, so you can assert on run counts, attempt numbers, and results after the fact. `TestConstraintChecker` replaces the production constraint checker with mutable fields that stand in for system state — network, battery, charging, idle time, and storage. When a task carries `Constraints`, the checker decides whether `doWork()` runs or the firing is recorded as `LastTaskResult.ConstraintsNotMet`. ## API reference ### Test a single task's doWork() `TestTaskRunner` is an object with one function. | Member | Signature | Behavior | |---|---|---| | `runTask(...)` | `suspend (task: DesktopTask, taskId: TaskId = TaskId("test-task"), inputData: TaskData = TaskData.EMPTY, runAttemptCount: Int = 1): TaskResult` | Runs `task.doWork()` with a `TaskContext` built from the arguments and returns its result. | ### Run tasks through the scheduler `TestDesktopTaskScheduler(constraintChecker: TestConstraintChecker? = null)` implements the scheduler backend and is `Closeable`. Use it with `use { }` so the platform default is always restored. ```kotlin import dev.nucleusframework.scheduler.DesktopTaskScheduler import dev.nucleusframework.scheduler.LastTaskResult import dev.nucleusframework.scheduler.TaskRequest import dev.nucleusframework.scheduler.testing.TestDesktopTaskScheduler import kotlin.time.Duration.Companion.hours TestDesktopTaskScheduler().use { testScheduler -> testScheduler.install() DesktopTaskScheduler.enqueue(TaskRequest.periodic(SyncId, 2.hours)) // Advance six hours — the 2-hour task fires three times val fired = testScheduler.advanceTimeBy(6.hours, registry) assertEquals(3, fired.size) val history = testScheduler.getExecutionHistory(SyncId) assertEquals(LastTaskResult.Success, history.last().result) } ``` | Member | Signature | Behavior | |---|---|---| | `install()` | `Unit` | Routes `DesktopTaskScheduler` calls to this instance. Installs `constraintChecker` if set. | | `uninstall()` | `Unit` | Restores the platform default backend (and constraint checker). Called by `close()`. | | `runTask(taskId, registry)` | `suspend (TaskId, TaskRegistry): TaskResult?` | Runs one enqueued task now. Returns `null` and records `ConstraintsNotMet` if its constraints fail. Throws if the task is not enqueued. | | `advanceTimeBy(duration, registry)` | `suspend (Duration, TaskRegistry): List` | Advances virtual time and fires every periodic task whose interval elapsed. Skips calendar and on-boot tasks. | | `currentVirtualTimeMs` | `Long` | The current virtual time. | | `getExecutionHistory(taskId)` | `(TaskId): List` | Chronological execution history for one task. | | `getAllExecutionHistory()` | `(): List` | Chronological execution history across all tasks. | | `getEnqueuedRequest(taskId)` | `(TaskId): TaskRequest?` | The stored request for a task, or `null`. | | `getEnqueuedRequests()` | `(): List` | Every currently enqueued request. | | `constraintChecker` | `TestConstraintChecker?` | The checker passed to the constructor, if any. | Each `ExecutionRecord` is a data class exposing `taskId: TaskId`, `result: LastTaskResult`, `runAttemptCount: Int`, and `virtualTimeMs: Long`. `advanceTimeBy()` only fires periodic tasks. Trigger a calendar or on-boot task explicitly with `runTask(taskId, registry)`. ### Simulate constraints `TestConstraintChecker()` is a `Closeable` checker with mutable fields for the system state that `Constraints` are evaluated against. Pass it to `TestDesktopTaskScheduler` to have its lifecycle co-managed, or call `install()` / `uninstall()` yourself. ```kotlin import dev.nucleusframework.scheduler.testing.TestConstraintChecker import dev.nucleusframework.scheduler.testing.TestDesktopTaskScheduler val checker = TestConstraintChecker() TestDesktopTaskScheduler(constraintChecker = checker).use { testScheduler -> testScheduler.install() checker.networkConnected = false // A task that requires network is skipped and recorded as ConstraintsNotMet val result = testScheduler.runTask(SyncId, registry) assertNull(result) } ``` | Field | Type | Default | Meaning | |---|---|---|---| | `networkConnected` | `Boolean` | `true` | Whether any network is active. | | `networkUnmetered` | `Boolean` | `true` | Whether the active network is unmetered (only when connected). | | `batteryLevel` | `Float?` | `1.0f` | Charge level `0.0`–`1.0`; `null` simulates no battery. | | `isCharging` | `Boolean` | `false` | Whether the device is plugged in. | | `idleTimeSeconds` | `Long` | `0L` | User idle time in seconds; `-1` simulates unavailable. | | `availableStorageBytes` | `Long` | `Long.MAX_VALUE` | Free disk space in bytes. | ## What's next - [Scheduler](/docs/lifecycle/scheduler) — register tasks, build requests, and configure constraints and retries. - [Lifecycle overview](/docs/lifecycle) — the rest of the OS-facing lifecycle modules. --- # Service Management (macOS) URL: https://nucleusframework.dev/en/docs/lifecycle/service-management Register login items, launch agents, and launch daemons through the macOS SMAppService API from Kotlin. `service-management-macos` lets you register login items, launch agents, and launch daemons through Apple's `SMAppService` API (macOS 13+) from Kotlin. It pairs with the Gradle plugin's `launchAgents { }` DSL, which embeds the corresponding plists in the app bundle at build time, so the services also work inside the Mac App Store sandbox where writing plists at runtime is not allowed. ## Add the dependency ```kotlin title="build.gradle.kts" dependencies { implementation("dev.nucleusframework:nucleus.service-management-macos:") } ``` `AppServiceManager.isAvailable` returns `false` on macOS versions below 13 and when the native library is not loaded. Check it before you register a service. ## Launch the app at login `MainApp` registers the app itself as a login item. It maps to `SMAppService.mainApp`, so no plist is required: ```kotlin import dev.nucleusframework.servicemanagement.AppService import dev.nucleusframework.servicemanagement.AppServiceManager import dev.nucleusframework.servicemanagement.AppServiceStatus AppServiceManager.register(AppService.MainApp) when (AppServiceManager.status(AppService.MainApp)) { AppServiceStatus.ENABLED -> { /* launches at next login */ } AppServiceStatus.REQUIRES_APPROVAL -> AppServiceManager.openSystemSettingsLoginItems() else -> {} } ``` For a cross-platform "launch at login", use [`AutoLaunch`](/docs/lifecycle/auto-launch) — it routes to `AppService.MainApp` on macOS. Use `service-management-macos` directly when you need agents or daemons. ## Register a background agent A launch agent runs in the user session on a schedule, independent of the main app window. 1. Declare the agent in `build.gradle.kts`: ```kotlin title="build.gradle.kts" nucleus.application { nativeDistributions { macOS { launchAgents { agent("com.myapp.background-sync") { arguments("--sync") startInterval(900) // 15 minutes } } } } } ``` The plugin generates `com.myapp.background-sync.plist` and embeds it in `Contents/Library/LaunchAgents/`. 2. Handle the agent argument in `main()`: ```kotlin fun main(args: Array) { if ("--sync" in args) { performSync() return } nucleusApplication(args) { /* ... */ } } ``` 3. Register the agent from your UI. macOS does not run the embedded plist until the app registers it and the user approves it: ```kotlin val agent = AppService.Agent("com.myapp.background-sync") AppServiceManager.register(agent) ``` The first registration shows a system notification ("MyApp wants to run in the background"). The user approves it there, or later from System Settings → General → Login Items. ## How it works `SMAppService` replaces the deprecated `SMLoginItemSetEnabled` and direct `launchctl` plist writes. Every background item is gated by user approval, listed in System Settings, and tied to a plist that ships inside the app bundle, so the entry is removed when the user deletes the app. The Gradle plugin's `launchAgents { }` DSL writes the plist with the expected keys (`Label`, `ProgramArguments`, `StartInterval` or `StartCalendarInterval`, `RunAtLoad`, `KeepAlive`, `ProcessType`) to `Contents/Library/LaunchAgents/