Nucleus
Tao backend

DecoratedWindow sur Tao

Ouvrez une fenêtre Compose Desktop sur le backend Tao avec un slot de barre de titre personnalisé et des contrôles de fenêtre natifs, sans AWT.

DecoratedWindow est le même Composable sur chaque backend Nucleus. Sur Tao, il ouvre une fenêtre OS native, monte une surface de rendu Skiko et vous donne un slot TitleBar que vous remplissez avec du contenu Compose — y compris des dispositions de boutons de contrôle propres à chaque OS. Cette page décrit le comportement spécifique à Tao de DecoratedWindow et les membres exposés dans son lambda de contenu.

Ajouter la dépendance

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

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

nucleus-application fournit le point d'entrée unifié ; decorated-window-tao fournit le backend Tao. Avec les deux sur le classpath, NucleusBackend.Tao devient disponible.

Ouvrir une fenêtre

Démarrez le runtime avec nucleusApplication, puis appelez DecoratedWindow et ajoutez une TitleBar :

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

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

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

Le slot TitleBar gère en interne l'appui-glisser sur la barre de titre : la fenêtre se déplace quand vous glissez une zone vide de la barre. Vous n'avez pas de modifier de drag à brancher vous-même.

Pour les mises en page plein fenêtre, le chrome d'un design system, les sidebars en verre macOS ou le Mica/Acrylic Windows 11, utilisez WindowScaffold à la place (ou autour) du TitleBar stock. Les sites d'appel TitleBar existants restent valides.

Ouvrir une fenêtre depuis la composition

DecoratedWindow / DecoratedDialog sont des extensions sur NucleusApplicationScope, un receiver qui n'existe qu'en tête de main(). Les fenêtres secondaires s'ouvrent en général depuis une destination de navigation ou une action de ligne, où ce receiver a disparu.

Depuis 2.3, nucleusApplication fournit LocalNucleusApplicationScope sur les deux backends (et propage les locals parentes dans chaque scène Tao). Les surcharges sans receiver le lisent :

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

@Composable
fun EditorWindow(onClose: () -> Unit) {
    // Pas de receiver d'application — lit LocalNucleusApplicationScope
    DecoratedWindow(onCloseRequest = onClose, title = "Editor") {
        EditorContent()
    }
}

@Composable
fun MaterialEditorWindow(onClose: () -> Unit) {
    // Les wrappers de toolkit ont encore besoin du scope en receiver :
    with(LocalNucleusApplicationScope.current) {
        MaterialDecoratedWindow(onCloseRequest = onClose) {
            EditorContent()
        }
    }
}

Hors de nucleusApplication { }, lire le local lève une erreur. Préférez les surcharges sans receiver de DecoratedWindow / DecoratedDialog pour les API de base ; utilisez LocalNucleusApplicationScope.current explicitement pour les wrappers de toolkit (MaterialDecoratedWindow, Jewel, Fluent, …) qui restent des extensions de scope.

Depuis 2.3.2, chaque scène Tao secondaire re-fournit aussi LocalTaoWindow et LocalTitleBarInfo pour cette fenêtre après le bridge des locals parentes. Le drag de barre de titre, le double-clic maximiser et les contrôles système ciblent donc la fenêtre enfant, pas le parent qui l'a ouverte. BasicTitleBar lie le drag avec Modifier.windowDragArea(window) pour rester correct même si un LocalTaoWindow parent reste visible dans l'arbre de composition.

Fenêtres hébergées

Les bibliothèques et couches de navigation ne doivent pas figer le Window / Dialog AWT de Compose Desktop (androidx.compose.ui.window, non supporté sous Tao) ni un type de fenêtre de design system. Depuis 2.3.2, nucleusApplication fournit aussi LocalNucleusWindowHost et LocalNucleusDialogHost. Les défauts ouvrent un DecoratedWindow / DecoratedDialog plain, et acceptent les réglages Tao popupFor, nativePopupLayers, nativeContextMenu, hiddenFromDock et alwaysOnBottom. Les autres drapeaux d'overlay — transparent, clickThrough, visibleOnAllWorkspaces et forceX11 — ne sont disponibles que sur DecoratedWindow lui-même. Les sites d'appel utilisent HostedWindow / HostedDialog :

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

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

// Chrome d'app : une seule surcharge, tous les HostedWindow la reprennent
@Composable
fun AppRoot() {
    CompositionLocalProvider(LocalNucleusWindowHost provides myMaterialHost) {
        NavigationGraph()
    }
}

Surchargez l'hôte quand l'application enveloppe chaque fenêtre secondaire dans Material, Jewel ou un chrome custom sans enseigner ce wrapper à chaque bibliothèque ou destination de navigation. Les appels directs à DecoratedWindow / DecoratedDialog restent valides quand vous voulez le type concret.

Comment ça marche

nucleusApplication { } résout le backend et expose DecoratedWindow sur NucleusApplicationScope. Sur Tao, cette surcharge délègue à dev.nucleusframework.window.tao.ApplicationScope.DecoratedWindow, qui ouvre un TaoWindow et monte une ComposeScene sur la surface native.

Dans le lambda de contenu, vous obtenez un NucleusDecoratedWindowScope. Sa propriété nucleusWindow renvoie le handle NucleusWindow agnostique au backend : état de focus, flows minimized / maximized / fullscreen, icône et taille minimale. Récupérez le TaoWindow brut via nucleusWindow.unsafe.taoWindow quand vous avez besoin d'un comportement propre à Tao.

Le style de la barre de titre est partagé avec les backends AWT. TitleBarStyle, TitleBarColors et TitleBarMetrics vivent dans decorated-window-core, et NucleusDecoratedWindowTheme les fournit via des composition locals. Le même thème fonctionne que la dépendance soit -tao, -jbr ou -jni.

Référence de l'API

Paramètres de DecoratedWindow

ParamètreTypeNotes
onCloseRequest() -> UnitÉmis par l'affordance de fermeture de l'OS.
stateWindowStatePosition, taille, placement. Un axe à Dp.Unspecified s'adapte au contenu après la première composition.
visibleBooleanDéfaut true.
titleStringTitre OS de la fenêtre.
iconPainter?Icône barre des tâches / dock.
resizableBooleanDéfaut true.
enabled / focusable / alwaysOnTopBooleanFlags de fenêtre standard.
undecoratedBooleanFenêtre sans cadre ni contrôles. Pris en compte par Tao ; ignoré par AWT.
isDialogBooleanDéfaut false. Ouvre la fenêtre en classe dialogue.
transparentBooleanDéfaut false. Transparence par pixel de la fenêtre. Uniquement à la création. Voir Fenêtres overlay.
clickThroughBooleanDéfaut false. Les événements pointeur traversent vers ce qui est derrière. Réactif.
alwaysOnBottomBooleanDéfaut false. Maintient la fenêtre sous les fenêtres normales. Annule alwaysOnTop. Réactif.
visibleOnAllWorkspacesBooleanDéfaut false. Affiche la fenêtre sur tous les bureaux ou Spaces. Réactif.
forceX11BooleanDéfaut false. Linux uniquement : prend une surface X11 (XWayland) pour cette seule fenêtre. Uniquement à la création.
nativeContextMenuBooleanDéfaut false. Remplace les menus contextuels Compose par ceux de la plateforme. Présent sur le DecoratedWindow agnostique du backend et sur HostedWindow, pas sur la surcharge de scope Tao. Voir Menus contextuels.
macOSStyleMacOSStyleDéfaut MacOSStyle.Classic. Traitement de la barre de titre sous macOS.
compositionLocalContextCompositionLocalContext?Composition locals à transporter dans la composition de la nouvelle fenêtre.
popupForNucleusWindow?Linux/Tao uniquement : attache la fenêtre comme popup superposée à une autre. Sous Wayland natif, les positions sont dans la zone contenu du parent (l'origine CSD title bar masquée est appliquée dans setOuterPosition).
nativePopupLayersBooleanDéfaut false. Matérialise les couches Compose Popup en fenêtres natives transparentes (NSPanel / WS_POPUP HWND). Tao uniquement.
hiddenFromDockBooleanDéfaut false. Masque la fenêtre de la barre des tâches / du Dock de l'OS. Pris en compte par Tao ; ignoré par AWT.
minimumSizeDpSize?Appliqué après la première passe de layout.
onPreviewKeyEvent / onKeyEvent(KeyEvent) -> BooleanRenvoie true pour consommer l'événement.
content@Composable NucleusDecoratedWindowScope.() -> UnitSlot barre de titre plus votre UI.

Le tableau ci-dessus décrit la surcharge Tao ApplicationScope.DecoratedWindow. Le NucleusApplicationScope.DecoratedWindow agnostique du backend, dans nucleus-application, accepte les mêmes drapeaux de fenêtre — dont transparent, clickThrough, alwaysOnBottom, visibleOnAllWorkspaces et forceX11 — et ajoute nativeContextMenu. Il n'expose pas macOSStyle, isDialog ni compositionLocalContext.

Les drapeaux que seul le backend Tao sait honorer sont acceptés et ignorés sous AWT : le même code compile et tourne donc sur l'un comme sur l'autre.

Masquer de la barre des tâches / du Dock

hiddenFromDock garde la fenêtre visible et focusable tout en la retirant de la liste des fenêtres au niveau OS — utile pour les HUD, les overlays et les fenêtres utilitaires en arrière-plan qui ne doivent pas encombrer la barre des tâches ou le sélecteur d'apps :

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

Le mécanisme diffère selon la plateforme :

  • macOS — bascule la NSApplication partagée vers la politique d'activation accessoire (pas d'icône dans le Dock, pas de barre de menu). C'est à l'échelle de l'app, pas par fenêtre : la dernière fenêtre qui applique le flag l'emporte.
  • Windows — positionne WS_EX_TOOLWINDOW sur la fenêtre, ce qui supprime son bouton dans la barre des tâches et son entrée dans Alt+Tab. Par fenêtre.
  • Linux — positionne les hints GTK skip-taskbar/skip-pager (_NET_WM_STATE_SKIP_TASKBAR). Par fenêtre, et effectif uniquement sous X11 ou XWayland.

Sous Wayland natif, hiddenFromDock n'a aucun effet — Wayland n'a pas de protocole client-side pour skip-taskbar (xdg-shell, gtk_shell1 et les extensions layer-shell en staging n'en ont pas, et Mutter rejette wlr-layer-shell). Nucleus journalise un avertissement plutôt que d'échouer silencieusement. Forcez XWayland avec NUCLEUS_TAO_LINUX_RENDERER=x11 si vous avez besoin que la fenêtre soit réellement masquée sur une session Wayland — voir Wayland natif.

Pour une app de barre de menus ou de tray qui doit rester hors du Dock tant qu'elle n'ouvre pas une vraie fenêtre, passez dockIconFollowsWindows = true à nucleusApplication plutôt que de définir LSUIElement dans l'Info.plist. L'app démarre en accessoire ; une tuile Dock n'apparaît que tant qu'au moins une DecoratedWindow avec hiddenFromDock = false est visible. Les popups tray standalone ne comptent jamais. Le flag est ignoré hors macOS et sur le backend AWT. Voir Masquer l'icône du Dock sur macOS.

Membres de scope

interface NucleusDecoratedWindowScope : DecoratedWindowScope {
    val nucleusWindow: NucleusWindow
}

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

Modifiers de barre de titre

  • Modifier.macOSLargeCornerRadius() — active les coins arrondis de macOS 26+.
  • Modifier.newFullscreenControls() — réagence les boutons de la barre de titre en plein écran.

Scaffold et chrome (2.2+)

Préférez ceux-ci quand le layout du TitleBar stock est trop rigide :

  • WindowScaffold / TitleBarPlacement — mises en page plein fenêtre et chrome en overlay.
  • Modifier.windowDragArea / noWindowDrag — déclarer (ou exclure) les zones de move natives.
  • WindowControls — min/max/fermer système hors de TitleBar.
  • WindowBackground / WindowAppearance — couleur de clear et clair/sombre natif depuis l'arbre.
  • Modifier.windowGlassRegion (macOS) / WindowsBackdrop (Windows 11) — matériaux de plateforme.

Référence complète : Scaffold de fenêtre et chrome.

TitleBar est une seule extension sur DecoratedWindowScope. Le scope de nucleusApplication (NucleusDecoratedWindowScope) et le scope Tao natif de taoApplication (TaoDecoratedWindowScope) l'implémentent tous les deux : le même appel à TitleBar fonctionne quel que soit le point d'entrée d'où vous partez. Idem pour WindowScaffold et les primitives de chrome.

Saisie de texte

La composition IME (japonais, chinois, coréen, et similaires) est transmise à Compose comme pré-édition sous macOS, Windows et Linux. Les touches que l'IME a déjà consommées — Entrée de conversion, flèches de candidats, Retour arrière sur le pré-edit — sont ignorées, pour ne pas insérer aussi un saut de ligne, déplacer le curseur ou supprimer du texte déjà validé. La fenêtre de candidats suit le curseur sur les trois plateformes.

Sous Linux, la méthode de saisie de la plateforme est résolue comme le font les widgets texte de GTK : ibus ou fcitx5 via l'immodule GTK sous X11, le client text-input-v3 sous Wayland. La pré-édition Linux demande 2.5.6 ou plus récent — les versions antérieures épinglaient le contexte de repli intégré de GTK, qui n'atteint jamais une méthode de saisie système.

Sous macOS, c'est le système qui décide seul si une lettre maintenue répète ou ouvre le sélecteur d'accents : Nucleus ne lit ni n'écrit ApplePressAndHoldEnabled. Un accent choisi est appliqué à partir du replacementRange qu'AppKit transmet avec lui, et remplace donc la lettre de base sans toucher au texte autour (2.5.6 ; les versions antérieures forçaient le default utilisateur et déduisaient l'état du sélecteur).

Gérer les exceptions non interceptées

Depuis 2.5.8, une exception qui s'échappe d'un dispatch d'événement Tao ne disparaît plus à la frontière JNI. Le chemin par défaut journalise la pile en SEVERE, affiche un dialogue natif bloquant, quitte la boucle Tao et relance l'exception pour que nucleusApplication / taoApplication se terminent avec le code de sortie 1. Le dialogue s'affiche après la sortie de la boucle — une pompe modale dans un callback tao provoque un deadlock du thread principal.

Depuis 2.5.9, le dialogue porte la pile complète dans une vue monospace scrollable bornée avec un bouton Copy : un NSAlert lancé par un processus osascript enfant sous macOS (repli compact CFUserNotification), un DLGTEMPLATE en mémoire sur un thread dédié sous Windows (repli MessageBoxW), et un GtkMessageDialog sous Linux.

Désactivez le dialogue pour les exécutions non supervisées (CI, entraînement AOT) avec -Dnucleus.tao.fatalErrorDialog=false. Le log SEVERE reste.

Pour garder une fenêtre vivante après une panne récupérable, fournissez un LocalWindowExceptionHandlerFactory. C'est le miroir Tao de la factory AWT de Compose Desktop, annoté @ExperimentalComposeUiApi. Les handlers s'exécutent sur le thread où la panne s'est produite, et couvrent le layout, le draw, l'entrée, l'IME, l'accessibilité et les popups nativePopupLayers de cette fenêtre :

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 ->
                // Retourner normalement abandonne la frame et laisse la fenêtre vivante.
                // Relancer prend le dialogue fatal et quitte.
            }
        },
    ) {
        DecoratedWindow(onCloseRequest = ::exitApplication, title = "Demo") {
            MyContent()
        }
    }
}

Retourner normalement est une vraie reprise pour le layout, le draw, l'entrée, l'IME et l'accessibilité : la frame fautive est abandonnée et une nouvelle est demandée. Une panne pendant la composition n'est pas récupérable — Compose arrête la boucle de recomposition avant que le backend ne voie l'exception, et cette boucle ne peut pas être relancée. L'avaler journalise en SEVERE au lieu de laisser une fenêtre qui repeint silencieusement sa dernière frame ; fermez et recréez la fenêtre, ou relancez pour prendre le chemin fatal. DefaultWindowExceptionHandlerFactory relance toujours.

Notes

  • macOS exige -XstartOnFirstThread. Le plugin Gradle Nucleus l'ajoute pour vous.
  • Pour les apps multi-fenêtres, appelez DecoratedWindow plusieurs fois depuis le même bloc nucleusApplication. Chaque fenêtre obtient son propre NucleusWindow.
  • Les fenêtres CSD Linux dessinent une ombre portée GTK native (motif hidden-titlebar) qui suit la fenêtre pendant les déplacements interactifs.
  • Les fenêtres CSD Windows sans Mica/Acrylic effacent le remplissage de la bordure DWM pour que le trait Compose du cadre reste visible, et dessinent le contour arrondi Win11 8 px via DWM pour que les coins ne soient pas clipés. Les fenêtres avec backdrop gardent le cadre système.
  • NucleusApplicationScope implémente l'ApplicationScope de Compose, si bien que les bibliothèques scopées au receiver Compose plain (par exemple les composables system-tray) se résolvent dans nucleusApplication { }.
  • Depuis 2.5.14, DecoratedWindow / DecoratedDialog (et les variantes hosted et Tao) sont @ComposableOpenTarget(-1) avec un lambda de contenu @UiComposable. Un composable ciblé non-UI appelé dans le bloc nucleusApplication ne reclassifie plus le contenu de la fenêtre, donc les appels @UiComposable à l'intérieur restent valides sous -Werror.

Et ensuite