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
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ètre | Type | Notes |
|---|---|---|
onCloseRequest | () -> Unit | Émis par l'affordance de fermeture de l'OS. |
state | WindowState | Position, taille, placement. Un axe à Dp.Unspecified s'adapte au contenu après la première composition. |
visible | Boolean | Défaut true. |
title | String | Titre OS de la fenêtre. |
icon | Painter? | Icône barre des tâches / dock. |
resizable | Boolean | Défaut true. |
enabled / focusable / alwaysOnTop | Boolean | Flags de fenêtre standard. |
undecorated | Boolean | Fenêtre sans cadre ni contrôles. Pris en compte par Tao ; ignoré par AWT. |
isDialog | Boolean | Défaut false. Ouvre la fenêtre en classe dialogue. |
transparent | Boolean | Défaut false. Transparence par pixel de la fenêtre. Uniquement à la création. Voir Fenêtres overlay. |
clickThrough | Boolean | Défaut false. Les événements pointeur traversent vers ce qui est derrière. Réactif. |
alwaysOnBottom | Boolean | Défaut false. Maintient la fenêtre sous les fenêtres normales. Annule alwaysOnTop. Réactif. |
visibleOnAllWorkspaces | Boolean | Défaut false. Affiche la fenêtre sur tous les bureaux ou Spaces. Réactif. |
forceX11 | Boolean | Défaut false. Linux uniquement : prend une surface X11 (XWayland) pour cette seule fenêtre. Uniquement à la création. |
nativeContextMenu | Boolean | Dé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. |
macOSStyle | MacOSStyle | Défaut MacOSStyle.Classic. Traitement de la barre de titre sous macOS. |
compositionLocalContext | CompositionLocalContext? | Composition locals à transporter dans la composition de la nouvelle fenêtre. |
popupFor | NucleusWindow? | 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). |
nativePopupLayers | Boolean | Défaut false. Matérialise les couches Compose Popup en fenêtres natives transparentes (NSPanel / WS_POPUP HWND). Tao uniquement. |
hiddenFromDock | Boolean | Dé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. |
minimumSize | DpSize? | Appliqué après la première passe de layout. |
onPreviewKeyEvent / onKeyEvent | (KeyEvent) -> Boolean | Renvoie true pour consommer l'événement. |
content | @Composable NucleusDecoratedWindowScope.() -> Unit | Slot 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
NSApplicationpartagé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_TOOLWINDOWsur 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 deTitleBar.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
DecoratedWindowplusieurs fois depuis le même blocnucleusApplication. Chaque fenêtre obtient son propreNucleusWindow. - 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.
NucleusApplicationScopeimplémente l'ApplicationScopede Compose, si bien que les bibliothèques scopées au receiver Compose plain (par exemple les composables system-tray) se résolvent dansnucleusApplication { }.- 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 blocnucleusApplicationne reclassifie plus le contenu de la fenêtre, donc les appels@UiComposableà l'intérieur restent valides sous-Werror.
Et ensuite
- Scaffold de fenêtre et chrome — mises en page plein fenêtre et matériaux.
- Le backend Tao — ce que couvre le backend sans AWT.
- DecoratedWindow sur tous les backends — l'API de fenêtre partagée.
- Multi-touch et gestes du trackpad — lire les événements d'entrée Tao.
- Backends — comment Nucleus choisit Tao ou AWT.
Le backend Tao
Tao est un backend de fenêtre natif en Rust qui exécute Compose Desktop sans AWT et ajoute Wayland natif, le multi-touch et le stylet.
Scaffold de fenêtre et chrome
Construisez des mises en page plein fenêtre et des barres de titre personnalisées sur Tao avec WindowScaffold, les zones de drag, WindowControls et les matériaux de plateforme.