From 3d145caa686fc8c1cac88b6936e15699383270bc Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Mon, 17 Aug 2026 17:10:54 +0300 Subject: [PATCH 01/12] feat(application): expose transparent on backend-agnostic DecoratedWindow Per-pixel window transparency (#416) was only reachable through the Tao backend's own DecoratedWindow composable. Forward the flag through both nucleus-application overloads and TaoDecoratedWindowAdapter; the AWT backend ignores it, like undecorated. --- nucleus-application/api/nucleus-application.api | 4 ++-- .../dev/nucleusframework/application/DecoratedWindow.kt | 8 ++++++++ .../application/internal/TaoDecoratedWindowAdapter.kt | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/nucleus-application/api/nucleus-application.api b/nucleus-application/api/nucleus-application.api index 273236b81..d885aee03 100644 --- a/nucleus-application/api/nucleus-application.api +++ b/nucleus-application/api/nucleus-application.api @@ -9,8 +9,8 @@ public final class dev/nucleusframework/application/DecoratedDialogKt { } public final class dev/nucleusframework/application/DecoratedWindowKt { - public static final fun DecoratedWindow-ghhko4k (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V - public static final fun DecoratedWindow-sr2hWdk (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun DecoratedWindow-OCZPU9E (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun DecoratedWindow-sr2hWdk (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V } public final class dev/nucleusframework/application/DefaultNucleusDialogHost : dev/nucleusframework/application/NucleusDialogHost { diff --git a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt index b0dc5af45..4f2c6937a 100644 --- a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt +++ b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt @@ -33,6 +33,11 @@ public fun NucleusApplicationScope.DecoratedWindow( // Fully borderless window (no macOS traffic lights) — for overlay/ghost windows. // Honoured by the Tao backend; the AWT backend currently ignores it. undecorated: Boolean = false, + // Full-window per-pixel transparency: pixels the content leaves at alpha 0 + // show the desktop behind the window (#416). Creation-time only — cannot + // change after the native window exists. Typically combined with + // [undecorated]. Honoured by the Tao backend; the AWT backend ignores it. + transparent: Boolean = false, // Linux/Tao only: make this window a popup overlay of [popupFor]. On // Wayland it maps as a wl_subsurface of the parent — the only window kind // a client can freely position under xdg-shell (coordinates are @@ -107,6 +112,7 @@ public fun NucleusApplicationScope.DecoratedWindow( focusable = focusable, alwaysOnTop = alwaysOnTop, undecorated = undecorated, + transparent = transparent, popupFor = popupFor, nativePopupLayers = nativePopupLayers, nativeContextMenu = nativeContextMenu, @@ -142,6 +148,7 @@ public fun DecoratedWindow( focusable: Boolean = true, alwaysOnTop: Boolean = false, undecorated: Boolean = false, + transparent: Boolean = false, popupFor: NucleusWindow? = null, nativePopupLayers: Boolean = false, nativeContextMenu: Boolean = false, @@ -162,6 +169,7 @@ public fun DecoratedWindow( focusable = focusable, alwaysOnTop = alwaysOnTop, undecorated = undecorated, + transparent = transparent, popupFor = popupFor, nativePopupLayers = nativePopupLayers, nativeContextMenu = nativeContextMenu, diff --git a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt index 72c444cae..702cd4c7d 100644 --- a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt +++ b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt @@ -48,6 +48,7 @@ internal object TaoDecoratedWindowAdapter { focusable: Boolean, alwaysOnTop: Boolean, undecorated: Boolean, + transparent: Boolean, popupFor: NucleusWindow?, nativePopupLayers: Boolean, nativeContextMenu: Boolean, @@ -88,6 +89,7 @@ internal object TaoDecoratedWindowAdapter { focusable = focusable, alwaysOnTop = alwaysOnTop, undecorated = undecorated, + transparent = transparent, popupFor = popupFor?.unsafe?.taoWindow, nativePopupLayers = nativePopupLayers, hiddenFromDock = hiddenFromDock, From 0beb5e7028a86f29ce189d8f3d6b53ef5e3919b2 Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Mon, 17 Aug 2026 17:21:41 +0300 Subject: [PATCH 02/12] fix(tao): drop window contour on fully transparent windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, a DecoratedWindow(transparent = true) with custom chrome still applied the themed DWM 1px frame (DWMWA_BORDER_COLOR), the drop shadow margin and the rounded clip — all tracing the rectangular HWND and betraying the content-defined shape. Reuse the borderlessChrome DWM treatment for fully transparent windows; hit-testing (title-bar drag, resize borders) is unaffected. Linux/Windows Compose CSD stroke is skipped for transparent windows for the same reason. --- .../window/tao/DecoratedWindow.kt | 19 ++++++++++++++----- .../tao/scene/TaoComposeSceneHostWindows.kt | 5 ++++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt index 99f9bcfa9..e1c99daea 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt @@ -313,8 +313,13 @@ internal fun ApplicationScope.openDecoratedWindow( transparent = transparent, // Tao defaults borderless windows to a drop shadow (DWM on // Windows, NSWindow.hasShadow on macOS). Overlays must opt out - // or the ghost still shows a soft contour. - undecoratedShadow = !undecorated, + // or the ghost still shows a soft contour. Fully transparent + // windows drop it on Windows too: the style-level shadow traces + // the rectangular HWND, not the content-defined shape (#416) — + // macOS keeps it (AppKit shapes the shadow to the drawn content) + // and Linux keeps its CSD hidden-titlebar path. + undecoratedShadow = + !undecorated && !(transparent && Platform.Current == Platform.Windows), ) // Compose Hot Reload: the agent only auto-wraps AWT `ComposeWindow`/ @@ -709,8 +714,10 @@ private fun ApplicationScope.openDecoratedWindowLinux( ) { // Default: CSD outline (vanilla-style frame for custom chrome). // `undecorated` = fully borderless overlay — no Compose stroke. + // `transparent` skips it too: the stroke traces the rectangular + // window bounds, not the content-defined shape (#416). val border = - if (undecorated) { + if (undecorated || transparent) { Modifier } else { rememberUndecoratedWindowBorder( @@ -1149,9 +1156,11 @@ private fun ApplicationScope.openDecoratedWindowWindows( } // Default: CSD outline for custom chrome windows. `undecorated` // means fully borderless (vanilla Compose Desktop semantics for - // overlays/ghosts) — do not stroke a frame. + // overlays/ghosts) — do not stroke a frame. `transparent` + // windows skip it too: the DWM 1px frame follows the + // rectangular HWND, not the content-defined shape (#416). val border = - if (undecorated) { + if (undecorated || transparent) { Modifier } else { rememberUndecoratedWindowBorder( diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/scene/TaoComposeSceneHostWindows.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/scene/TaoComposeSceneHostWindows.kt index 080cf7bc2..741209872 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/scene/TaoComposeSceneHostWindows.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/scene/TaoComposeSceneHostWindows.kt @@ -290,9 +290,12 @@ internal class TaoComposeSceneHostWindows( (titleBarHeightDpState.value * scale).toInt().coerceAtLeast(28) } NativeTaoWindowsDecoBridge.nativeInstallDecoration(hwnd, initialTitleBarPx) - if (borderlessChrome) { + if (borderlessChrome || fullyTransparent) { // Kill DWM 1px contour + shadow margin (Compose border is already // skipped by the openDecoratedWindowWindows undecorated path). + // Fully transparent windows (#416) need the same treatment even + // with chrome: the DWM frame, drop shadow and rounded clip all + // trace the rectangular HWND, betraying the content-defined shape. NativeTaoWindowsDecoBridge.nativeSetBorderlessChrome(hwnd, true) } From 6b303908457fb42ae38c3e0ae0e9e9570992cc2b Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Mon, 17 Aug 2026 17:54:42 +0300 Subject: [PATCH 03/12] feat(tao): click-through windows via clickThrough / setIgnoreCursorEvents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plumb tao's set_ignore_cursor_events through the JNI bridge and expose it as TaoWindow.setIgnoreCursorEvents plus a reactive clickThrough parameter on both the Tao and backend-agnostic DecoratedWindow composables (WS_EX_TRANSPARENT|WS_EX_LAYERED on Windows, NSWindow.ignoresMouseEvents on macOS, empty GDK input region on Linux). On Windows, tao only flips the ex-styles; a WS_EX_LAYERED window renders nothing until its layering attributes are initialised, so the native handler calls SetLayeredWindowAttributes(alpha=255) — full alpha keeps per-pixel transparency driven by DWM blur-behind. --- .../api/decorated-window-tao.api | 3 +- .../window/tao/DecoratedWindowComposable.kt | 10 ++++++ .../nucleusframework/window/tao/TaoWindow.kt | 13 ++++++- .../window/tao/ffi/NativeTaoBridge.kt | 6 ++++ .../src/main/native/src/event_loop.rs | 34 +++++++++++++++++++ .../src/main/native/src/events.rs | 4 +++ .../src/main/native/src/window_jni.rs | 13 +++++++ .../api/nucleus-application.api | 4 +-- .../application/DecoratedWindow.kt | 8 +++++ .../internal/TaoDecoratedWindowAdapter.kt | 2 ++ 10 files changed, 93 insertions(+), 4 deletions(-) diff --git a/decorated-window-tao/api/decorated-window-tao.api b/decorated-window-tao/api/decorated-window-tao.api index 39055b7bb..167eb8af5 100644 --- a/decorated-window-tao/api/decorated-window-tao.api +++ b/decorated-window-tao/api/decorated-window-tao.api @@ -198,7 +198,7 @@ public final class dev/nucleusframework/window/tao/DecoratedDialogKt { } public final class dev/nucleusframework/window/tao/DecoratedWindowComposableKt { - public static final fun DecoratedWindow-KMb5BrY (Ldev/nucleusframework/window/tao/ApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;Ljava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/DpSize;ZZZZZZZZLdev/nucleusframework/window/tao/TaoWindow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLdev/nucleusframework/window/tao/MacOSStyle;ZLandroidx/compose/runtime/CompositionLocalContext;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun DecoratedWindow-4Q_Ebkg (Ldev/nucleusframework/window/tao/ApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;Ljava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/DpSize;ZZZZZZZZZLdev/nucleusframework/window/tao/TaoWindow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLdev/nucleusframework/window/tao/MacOSStyle;ZLandroidx/compose/runtime/CompositionLocalContext;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class dev/nucleusframework/window/tao/DecoratedWindowKt { @@ -747,6 +747,7 @@ public final class dev/nucleusframework/window/tao/TaoWindow { public final fun setFocusable (Z)V public final fun setFullscreen (Z)V public final fun setIcon (II[B)V + public final fun setIgnoreCursorEvents (Z)V public final fun setInnerSize (DD)V public final fun setMaximized (Z)V public final fun setMinimized (Z)V diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt index 86fa429d7..74079ebef 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt @@ -65,6 +65,15 @@ public fun ApplicationScope.DecoratedWindow( enabled: Boolean = true, focusable: Boolean = true, alwaysOnTop: Boolean = false, + /** + * Click-through window: every pointer event falls through to whatever + * sits below (`WS_EX_TRANSPARENT | WS_EX_LAYERED` on Windows, + * `NSWindow.ignoresMouseEvents` on macOS, an empty GDK input region on + * Linux). Reactive — can be toggled at runtime. Pair with + * `focusable = false` for passive overlays (watermarks, HUDs) that must + * never intercept input. + */ + clickThrough: Boolean = false, isDialog: Boolean = false, /** * Fully borderless window — for overlays/ghosts (drag previews, HUDs). @@ -415,6 +424,7 @@ public fun ApplicationScope.DecoratedWindow( LaunchedEffect(window, title) { window.setTitle(title) } LaunchedEffect(window, alwaysOnTop) { window.setAlwaysOnTop(alwaysOnTop) } LaunchedEffect(window, focusable) { window.setFocusable(focusable) } + LaunchedEffect(window, clickThrough) { window.setIgnoreCursorEvents(clickThrough) } LaunchedEffect(window, visible) { if (visible) { window.show() diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt index 34f15f046..5969be61f 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt @@ -19,7 +19,7 @@ import java.util.concurrent.atomic.AtomicBoolean * event loop, which executes them on the platform event-loop thread. Listener * registration is also safe to call across threads. */ -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass") public class TaoWindow internal constructor( public val handle: Long, isResizable: Boolean = true, @@ -637,6 +637,17 @@ public class TaoWindow internal constructor( NativeTaoBridge.nativeSetFocusable(handle, focusable) } + /** + * Makes the window click-through: every pointer event falls through to + * whatever sits below it (`WS_EX_TRANSPARENT | WS_EX_LAYERED` on Windows, + * `NSWindow.ignoresMouseEvents` on macOS, an empty GDK input region on + * Linux). Pair with [setFocusable]`(false)` for passive overlays such as + * watermarks or HUDs that must never intercept input. + */ + public fun setIgnoreCursorEvents(ignore: Boolean) { + NativeTaoBridge.nativeSetIgnoreCursorEvents(handle, ignore) + } + /** Logical pixels. Pass `null` to clear the minimum. */ public fun setMinimumSize( widthDp: Double?, diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt index 776da0abd..231fee285 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt @@ -440,6 +440,12 @@ internal object NativeTaoBridge { focusable: Boolean, ) + @JvmStatic + external fun nativeSetIgnoreCursorEvents( + handle: Long, + ignore: Boolean, + ) + /** * Raises the window to the top of the z-order, restores it if minimized, * and gives it keyboard focus. Maps to Tao's `Window::set_focus()` which diff --git a/decorated-window-tao/src/main/native/src/event_loop.rs b/decorated-window-tao/src/main/native/src/event_loop.rs index 4f9ce59ec..283fd1b82 100644 --- a/decorated-window-tao/src/main/native/src/event_loop.rs +++ b/decorated-window-tao/src/main/native/src/event_loop.rs @@ -464,6 +464,40 @@ pub(crate) fn run_event_loop_blocking() { } } } + UserEvent::SetIgnoreCursorEvents { handle, ignore } => { + // Click-through: WS_EX_TRANSPARENT|WS_EX_LAYERED on + // Windows, NSWindow.ignoresMouseEvents on macOS, an empty + // GDK input region on Linux. + let guard = WINDOWS.lock().unwrap(); + if let Some(map) = guard.as_ref() { + if let Some(w) = map.get(&handle) { + let _ = w.set_ignore_cursor_events(ignore); + // tao only flips the ex-styles. A WS_EX_LAYERED + // window renders NOTHING until its layering + // attributes are initialised — without this the + // whole window disappears the moment click-through + // is enabled. Full alpha keeps per-pixel + // transparency driven by DWM blur-behind. + #[cfg(target_os = "windows")] + if ignore { + use tao::platform::windows::WindowExtWindows; + use windows::Win32::Foundation::{COLORREF, HWND}; + use windows::Win32::UI::WindowsAndMessaging::{ + SetLayeredWindowAttributes, LWA_ALPHA, + }; + let hwnd = HWND(w.hwnd() as *mut _); + unsafe { + let _ = SetLayeredWindowAttributes( + hwnd, + COLORREF(0), + 255, + LWA_ALPHA, + ); + } + } + } + } + } UserEvent::Focus { handle } => { let guard = WINDOWS.lock().unwrap(); if let Some(map) = guard.as_ref() { diff --git a/decorated-window-tao/src/main/native/src/events.rs b/decorated-window-tao/src/main/native/src/events.rs index 6d7c2ded4..a707c830b 100644 --- a/decorated-window-tao/src/main/native/src/events.rs +++ b/decorated-window-tao/src/main/native/src/events.rs @@ -322,6 +322,10 @@ pub(crate) enum UserEvent { handle: u64, focusable: bool, }, + SetIgnoreCursorEvents { + handle: u64, + ignore: bool, + }, Focus { handle: u64, }, diff --git a/decorated-window-tao/src/main/native/src/window_jni.rs b/decorated-window-tao/src/main/native/src/window_jni.rs index 739b3c54e..72f27179e 100644 --- a/decorated-window-tao/src/main/native/src/window_jni.rs +++ b/decorated-window-tao/src/main/native/src/window_jni.rs @@ -385,6 +385,19 @@ pub extern "system" fn Java_dev_nucleusframework_window_tao_ffi_NativeTaoBridge_ }); } +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_window_tao_ffi_NativeTaoBridge_nativeSetIgnoreCursorEvents( + _env: JNIEnv, + _class: JClass, + handle: jlong, + ignore: jboolean, +) { + send_user_event(UserEvent::SetIgnoreCursorEvents { + handle: handle as u64, + ignore: ignore != JNI_FALSE, + }); +} + #[no_mangle] pub extern "system" fn Java_dev_nucleusframework_window_tao_ffi_NativeTaoBridge_nativeSetMinInnerSize( _env: JNIEnv, diff --git a/nucleus-application/api/nucleus-application.api b/nucleus-application/api/nucleus-application.api index d885aee03..662f27727 100644 --- a/nucleus-application/api/nucleus-application.api +++ b/nucleus-application/api/nucleus-application.api @@ -9,8 +9,8 @@ public final class dev/nucleusframework/application/DecoratedDialogKt { } public final class dev/nucleusframework/application/DecoratedWindowKt { - public static final fun DecoratedWindow-OCZPU9E (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V - public static final fun DecoratedWindow-sr2hWdk (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun DecoratedWindow-O3YsXe4 (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun DecoratedWindow-OCZPU9E (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V } public final class dev/nucleusframework/application/DefaultNucleusDialogHost : dev/nucleusframework/application/NucleusDialogHost { diff --git a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt index 4f2c6937a..32230e273 100644 --- a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt +++ b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt @@ -38,6 +38,11 @@ public fun NucleusApplicationScope.DecoratedWindow( // change after the native window exists. Typically combined with // [undecorated]. Honoured by the Tao backend; the AWT backend ignores it. transparent: Boolean = false, + // Click-through window: pointer events fall through to whatever sits + // below, and the window never intercepts input. Pair with + // `focusable = false` for passive overlays (watermarks, HUDs). Reactive. + // Honoured by the Tao backend; the AWT backend ignores it. + clickThrough: Boolean = false, // Linux/Tao only: make this window a popup overlay of [popupFor]. On // Wayland it maps as a wl_subsurface of the parent — the only window kind // a client can freely position under xdg-shell (coordinates are @@ -113,6 +118,7 @@ public fun NucleusApplicationScope.DecoratedWindow( alwaysOnTop = alwaysOnTop, undecorated = undecorated, transparent = transparent, + clickThrough = clickThrough, popupFor = popupFor, nativePopupLayers = nativePopupLayers, nativeContextMenu = nativeContextMenu, @@ -149,6 +155,7 @@ public fun DecoratedWindow( alwaysOnTop: Boolean = false, undecorated: Boolean = false, transparent: Boolean = false, + clickThrough: Boolean = false, popupFor: NucleusWindow? = null, nativePopupLayers: Boolean = false, nativeContextMenu: Boolean = false, @@ -170,6 +177,7 @@ public fun DecoratedWindow( alwaysOnTop = alwaysOnTop, undecorated = undecorated, transparent = transparent, + clickThrough = clickThrough, popupFor = popupFor, nativePopupLayers = nativePopupLayers, nativeContextMenu = nativeContextMenu, diff --git a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt index 702cd4c7d..0e801089a 100644 --- a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt +++ b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt @@ -49,6 +49,7 @@ internal object TaoDecoratedWindowAdapter { alwaysOnTop: Boolean, undecorated: Boolean, transparent: Boolean, + clickThrough: Boolean, popupFor: NucleusWindow?, nativePopupLayers: Boolean, nativeContextMenu: Boolean, @@ -90,6 +91,7 @@ internal object TaoDecoratedWindowAdapter { alwaysOnTop = alwaysOnTop, undecorated = undecorated, transparent = transparent, + clickThrough = clickThrough, popupFor = popupFor?.unsafe?.taoWindow, nativePopupLayers = nativePopupLayers, hiddenFromDock = hiddenFromDock, From fcbd9a8c83896838a56c74f35b56a3095f9218a4 Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Mon, 17 Aug 2026 17:54:42 +0300 Subject: [PATCH 04/12] =?UTF-8?q?feat(examples):=20watermark-demo=20?= =?UTF-8?q?=E2=80=94=20transparent=20click-through=20overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Animated NUCLEUS watermark in a transparent, undecorated, always-on-top, click-through, never-focused window pinned to a screen corner, hidden from the taskbar and controlled entirely from a ComposeNativeTray icon (corner selection + quit). The tray icon is fed as image files (iconPath/windowsIconPath as file: URIs): the composable-icon overloads crash on the skiko shipped with Compose 1.12 (NoSuchMethodError on Image.encodeToData), and java.net.URI rejects raw Windows paths. --- build.gradle.kts | 1 + examples/watermark-demo/build.gradle.kts | 32 +++ .../main/kotlin/com/example/watermark/Main.kt | 198 ++++++++++++++++++ .../src/main/resources/tray-icon.ico | Bin 0 -> 1178 bytes .../src/main/resources/tray-icon.png | Bin 0 -> 1156 bytes gradle/libs.versions.toml | 2 + settings.gradle.kts | 1 + 7 files changed, 234 insertions(+) create mode 100644 examples/watermark-demo/build.gradle.kts create mode 100644 examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt create mode 100644 examples/watermark-demo/src/main/resources/tray-icon.ico create mode 100644 examples/watermark-demo/src/main/resources/tray-icon.png diff --git a/build.gradle.kts b/build.gradle.kts index ebe77e6ac..46d5b2fc5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,6 +45,7 @@ apiValidation { "avfoundation-demo", "tao-native-test", "window-scaffold-demo", + "watermark-demo", // BCV 0.18.1's bundled ASM cannot read JVM 25 class files (major 69). // Module still uses explicitApi(); re-enable once BCV/KGP ABI supports it. "decorated-window-jewel", diff --git a/examples/watermark-demo/build.gradle.kts b/examples/watermark-demo/build.gradle.kts new file mode 100644 index 000000000..b0fb1a67d --- /dev/null +++ b/examples/watermark-demo/build.gradle.kts @@ -0,0 +1,32 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("jvm") + alias(libs.plugins.kotlinComposePlugin) + alias(libs.plugins.jetbrainsCompose) + id("dev.nucleusframework") +} + +dependencies { + implementation(project(":decorated-window-tao")) + implementation(project(":nucleus-application")) + implementation(project(":core-runtime")) + implementation(compose.desktop.currentOs) + implementation(libs.composenativetray) +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + +nucleus.application { + mainClass = "com.example.watermark.MainKt" + graalvm.isEnabled.set(true) +} diff --git a/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt new file mode 100644 index 000000000..4ad42c6aa --- /dev/null +++ b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt @@ -0,0 +1,198 @@ +package com.example.watermark + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberWindowState +import dev.nucleusframework.application.DecoratedWindow +import dev.nucleusframework.application.nucleusApplication +import dev.nucleusframework.composenativetray.tray.api.Tray +import java.io.File + +/** + * Watermark overlay demo. + * + * Showcases full-window per-pixel transparency (#416) on the backend-agnostic + * `DecoratedWindow`: a borderless, always-on-top, taskbar-hidden window pinned + * to a screen corner, whose only visible pixels are an animated "NUCLEUS" + * watermark pill — everything else shows the desktop through. + * + * The app has no Dock/taskbar presence (`hiddenFromDock`), never takes focus + * (`focusable = false`) and lets every click fall through to whatever sits + * underneath (`clickThrough`); it is controlled entirely from its tray icon + * (ComposeNativeTray): pick the corner the watermark is pinned to, or quit. + * Corner changes go through `WindowState.position = WindowPosition.Aligned(...)`, + * which the Tao backend resolves against the primary monitor's work area at + * runtime. + * + * The tray icon is fed as image *files* (the `iconPath` overload): the + * composable-icon overloads render through a skiko `Image.encodeToData` + * signature that no longer exists in the skiko shipped with Compose 1.12, and + * crash with `NoSuchMethodError` until ComposeNativeTray is rebuilt against it. + */ +fun main(args: Array) = + nucleusApplication(args) { + val trayIconPng = remember { extractToTempFile("/tray-icon.png", ".png") } + val trayIconIco = remember { extractToTempFile("/tray-icon.ico", ".ico") } + val state = + rememberWindowState( + position = WindowPosition.Aligned(Alignment.BottomEnd), + size = DpSize(360.dp, 160.dp), + ) + var corner by remember { mutableStateOf(Alignment.BottomEnd) } + + fun moveTo(alignment: Alignment) { + corner = alignment + state.position = WindowPosition.Aligned(alignment) + } + + Tray( + iconPath = trayIconPng, + windowsIconPath = trayIconIco, + tooltip = "Nucleus watermark", + ) { + CheckableItem( + label = "Top left", + checked = corner == Alignment.TopStart, + onCheckedChange = { moveTo(Alignment.TopStart) }, + ) + CheckableItem( + label = "Top right", + checked = corner == Alignment.TopEnd, + onCheckedChange = { moveTo(Alignment.TopEnd) }, + ) + CheckableItem( + label = "Bottom left", + checked = corner == Alignment.BottomStart, + onCheckedChange = { moveTo(Alignment.BottomStart) }, + ) + CheckableItem( + label = "Bottom right", + checked = corner == Alignment.BottomEnd, + onCheckedChange = { moveTo(Alignment.BottomEnd) }, + ) + Divider() + Item(label = "Quit") { exitApplication() } + } + + DecoratedWindow( + onCloseRequest = ::exitApplication, + state = state, + title = "Watermark", + resizable = false, + alwaysOnTop = true, + undecorated = true, + transparent = true, + hiddenFromDock = true, + // A watermark must never get in the way: clicks fall through to + // whatever is underneath and the window never takes focus. + clickThrough = true, + focusable = false, + ) { + AnimatedWatermark() + } + } + +/** + * Copies a classpath resource to a temp file and returns it as a `file:` URI + * string — ComposeNativeTray parses `iconPath` through `java.net.URI`, which + * rejects raw Windows paths (`C:\…` reads as an opaque `C:` scheme). + */ +private fun extractToTempFile( + resource: String, + suffix: String, +): String { + val bytes = + checkNotNull(object {}.javaClass.getResourceAsStream(resource)) { + "Missing resource $resource" + }.use { it.readBytes() } + val file = File.createTempFile("watermark-tray", suffix) + file.writeBytes(bytes) + file.deleteOnExit() + return file.toURI().toString() +} + +/** + * The watermark pill: a translucent rounded capsule whose "NUCLEUS" label + * breathes (alpha pulse) while a light shimmer sweeps across the glyphs. + * Everything outside the pill stays at alpha 0 — over a transparent window + * that means the desktop. + */ +@Composable +private fun AnimatedWatermark() { + val transition = rememberInfiniteTransition(label = "watermark") + val pulse by transition.animateFloat( + initialValue = 0.45f, + targetValue = 1f, + animationSpec = + infiniteRepeatable( + animation = tween(durationMillis = 1600, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "pulse", + ) + val sweep by transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = + infiniteRepeatable( + animation = tween(durationMillis = 2600, easing = LinearEasing), + ), + label = "sweep", + ) + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + // Sweep a bright band across the text; the band travels a bit past + // both ends so the shimmer fully leaves the glyphs between passes. + val sweepX = sweep * 700f - 200f + val shimmer = + Brush.linearGradient( + colors = listOf(Color(0x66FFFFFF), Color.White, Color(0x66FFFFFF)), + start = Offset(sweepX - 150f, 0f), + end = Offset(sweepX + 150f, 140f), + ) + BasicText( + text = "NUCLEUS", + modifier = + Modifier + .graphicsLayer { alpha = pulse } + .clip(RoundedCornerShape(28.dp)) + .background(Color.Black.copy(alpha = 0.35f)) + .padding(horizontal = 28.dp, vertical = 14.dp), + style = + TextStyle( + brush = shimmer, + fontSize = 34.sp, + fontWeight = FontWeight.Black, + letterSpacing = 8.sp, + ), + ) + } +} diff --git a/examples/watermark-demo/src/main/resources/tray-icon.ico b/examples/watermark-demo/src/main/resources/tray-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..763cd6374b56fdb3c48249460464b9cb8021cb1f GIT binary patch literal 1178 zcmV;L1ZDdG0096206;(h0096X0E7ep02TlM0EtjeM-2)Z3IG5A4M|8uQUCw|KmY&$ zKnMl^0063Kaozv`00DDSM?wIu&K&6g000DMK}|sb0I`n?{9y$E000SaNLh0L01m?d z01m?e$8V@)000CTNklB7B9WDqb**e*CRbUBtZY#9cAKlKBq1Tl z${Ld5ke6UEBqvYes6bTW-fQ0OUAy<@XXkf!j_@U)Ic8?(eLL@GW|p0uXFOvROeMUj z4)9%uqY=V1J37=~LSG9yiEfM<(%UZ&`5_tSY;4406@tE%8>q_Fc)(~!72w_y_Hp;CfFH`_hNOV2QQ}1F z=kQyZ&^#Ocr9;%tln53-1=b9XCbXOl<3M&aeXK(QZpLY_B?(eP?aFbkw zTEchbK?Spt@Qd3J4DxyYfNdbTE4RfPP0e0lz;k9ycAz@GlvEc=dU?Th0{Iaia61VL zZd)&!@>Uth&g*+LVtJ~zxAE+fkI=$*oPYFng^=3bCIH?eo@x4+m|Oq}3+L2GYN-SC z(<%TWK3uAOOiV8TBArWcTeS$?()I~JxIR+*>6k+R3h2G7pxx7}on7c6fU7Iy5Maea zZu16WDn2Ix99ip{oFd3r+tnGYar%bOS0Fcn2T)2!OY0|1NV0 zK&&LZ=e9S9-RhbIcrg5KGbX11w5#Q|H=u1KZ|p;p02inHm;W%iLQVlF8*tkj#Mb>L z0Vwrd-11*nFt|ca0f^c-z#I9}CP00J_`v_Lg042mF2Dr$HjJkqcq3n01+W;Us|`$T zQwg9PsnfonH}Ykl0Jb)~Hpndi9rA1l5L*9tX)N`0w`I4 z3IpEAm%aiJm%Q-`OM^J61kfIoRs#O;hCU1tz~%}%q!M5@!I#{AItz&Uq9aHEn=5FL zLIBzlbK4uxXDGa}4?_j8)dn1i1khib#b$zF0W49UBZUA;2IwLt4#Wsx>)%BR0kr0!q!55;stpJ+0yGDr`?$+( zR?vLesTrV;sRXdq1~CFqmgBZ+5wi+PB?Wh!^#~BjR`PMy6kNzFFU|FpJR43b;8JTVO6q+=lK+%)iCtlfdU=f z?hLl%`#=++2@UdLwSW_^jm?mtiM8<|b=?39y3SJW^-D&RHYFa?DQwWn5O778w#B|} sUo>vA{$F8%$F0kBxcb|ZF?bvB7h_#;{OV07*qoM6N<$f}vgk6#xJL literal 0 HcmV?d00001 diff --git a/examples/watermark-demo/src/main/resources/tray-icon.png b/examples/watermark-demo/src/main/resources/tray-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e7a6eb20510d6ca5aa18854be066596d3d51600a GIT binary patch literal 1156 zcmV-~1bh35P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1Q|(0K~#8N?OQEO z96=ODR#qaBm6dg^Y+xo=S&6J{Q1o`2tE(g-A;`)alH!n;U@#;nPvWRRRN~%i-tJwy z_vUBkcXy8PC7(HFX6Jo7?`LL~otK63@d~6=7nar4^ ztik7+9Bp)0A6^P82|s6V@dclE5{&Net`u;ST!dP}cjZ9^vy$+O+Yk)$dH#THAh|2I z#T!k{USPm;W=wXVI=+-t7fO10!F2-p5g%|n2@7spFPidJ8OYA-do*Hss03tqIs(nmMF90H)OK@AY2;I{5 z2|&0$Qv2zcLjVfsy{n+z)2p3b=pultE94Mh#Y1lM24X5cCjlH?A(sHOSKu~ppgD2T zbabJs0E7!o|1L8M@Mv@cDj0YNANmM@w`%_`a|uAKB)sRgH;CQpngn<-{BAQQrvS98 z<+eAVZ6t5(Lz4g(r~H@yFu6ib0Vo@A+Z)8z{U!k@^i29-~qMG675{=Y`d6}ZU&R{>mHAzlEObDK91vnPla zz`+$_1yJ??98?d49*m?AfM}`>2r&XQ2crA9%WYQBeA%fPppU5pu+;`J0#KIYwrUZx z3Q8pfcc-_%MocUyk-fgJL+Bl0X>__1)FHMoDv0L7rw^e4u^74rJ{xKY=gPoa!JeYW z742C8%+&`JOaXV+lGhhBSj^4wkffBSJ@AMH<w&eRj6QKzW@?o`r6R(ZUkf4dR z@ga5H01LX#QttIjMw2!r9?~gn(8>^SMVGe4zHMJLZnFMgVS&f3%XGN<+mkVP8}Jun WU2y)mqS_w-0000 Date: Mon, 17 Aug 2026 18:20:13 +0300 Subject: [PATCH 05/12] feat(tao): show overlays on every desktop via visibleOnAllWorkspaces An NSWindow stays bound to the Space it was created in, and a GTK window to its workspace, so the watermark overlay vanished on macOS/Linux as soon as the user switched desktop. Windows needed nothing: a WS_EX_TOOLWINDOW (hiddenFromDock) window is not tracked by the Virtual Desktop Manager and already shows everywhere. Wire tao's set_visible_on_all_workspaces (CanJoinAllSpaces / gtk stick, no-op on Windows) as a reactive DecoratedWindow parameter, and move the new overlay flags to the end of the public parameter lists so existing positional call sites keep their meaning. --- .../api/decorated-window-tao.api | 3 +- .../window/tao/DecoratedWindowComposable.kt | 35 ++++++++++++---- .../nucleusframework/window/tao/TaoWindow.kt | 19 +++++++++ .../window/tao/ffi/NativeTaoBridge.kt | 6 +++ .../src/main/native/src/event_loop.rs | 15 +++++++ .../src/main/native/src/events.rs | 4 ++ .../src/main/native/src/window_jni.rs | 13 ++++++ .../main/kotlin/com/example/watermark/Main.kt | 14 +++++-- .../api/nucleus-application.api | 4 +- .../application/DecoratedWindow.kt | 41 ++++++++++++------- .../internal/TaoDecoratedWindowAdapter.kt | 2 + 11 files changed, 126 insertions(+), 30 deletions(-) diff --git a/decorated-window-tao/api/decorated-window-tao.api b/decorated-window-tao/api/decorated-window-tao.api index 167eb8af5..168d5737e 100644 --- a/decorated-window-tao/api/decorated-window-tao.api +++ b/decorated-window-tao/api/decorated-window-tao.api @@ -198,7 +198,7 @@ public final class dev/nucleusframework/window/tao/DecoratedDialogKt { } public final class dev/nucleusframework/window/tao/DecoratedWindowComposableKt { - public static final fun DecoratedWindow-4Q_Ebkg (Ldev/nucleusframework/window/tao/ApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;Ljava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/DpSize;ZZZZZZZZZLdev/nucleusframework/window/tao/TaoWindow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLdev/nucleusframework/window/tao/MacOSStyle;ZLandroidx/compose/runtime/CompositionLocalContext;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun DecoratedWindow-krRzeA4 (Ldev/nucleusframework/window/tao/ApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;Ljava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/DpSize;ZZZZZZZZLdev/nucleusframework/window/tao/TaoWindow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLdev/nucleusframework/window/tao/MacOSStyle;ZLandroidx/compose/runtime/CompositionLocalContext;ZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class dev/nucleusframework/window/tao/DecoratedWindowKt { @@ -755,6 +755,7 @@ public final class dev/nucleusframework/window/tao/TaoWindow { public final fun setOuterPosition (DD)V public final fun setResizable (Z)V public final fun setTitle (Ljava/lang/String;)V + public final fun setVisibleOnAllWorkspaces (Z)V public final fun show ()V public final fun xdgPortalParent (J)Ldev/nucleusframework/window/tao/XdgPortalParent; public static synthetic fun xdgPortalParent$default (Ldev/nucleusframework/window/tao/TaoWindow;JILjava/lang/Object;)Ldev/nucleusframework/window/tao/XdgPortalParent; diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt index 74079ebef..8cfebb235 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt @@ -65,15 +65,6 @@ public fun ApplicationScope.DecoratedWindow( enabled: Boolean = true, focusable: Boolean = true, alwaysOnTop: Boolean = false, - /** - * Click-through window: every pointer event falls through to whatever - * sits below (`WS_EX_TRANSPARENT | WS_EX_LAYERED` on Windows, - * `NSWindow.ignoresMouseEvents` on macOS, an empty GDK input region on - * Linux). Reactive — can be toggled at runtime. Pair with - * `focusable = false` for passive overlays (watermarks, HUDs) that must - * never intercept input. - */ - clickThrough: Boolean = false, isDialog: Boolean = false, /** * Fully borderless window — for overlays/ghosts (drag previews, HUDs). @@ -139,6 +130,29 @@ public fun ApplicationScope.DecoratedWindow( // the first composition (see [openDecoratedWindow]). Defaults to null for // top-level windows; [DecoratedDialog] forwards its parent's locals here. compositionLocalContext: CompositionLocalContext? = null, + /** + * Click-through window: every pointer event falls through to whatever + * sits below (`WS_EX_TRANSPARENT | WS_EX_LAYERED` on Windows, + * `NSWindow.ignoresMouseEvents` on macOS, an empty GDK input region on + * Linux). Reactive — can be toggled at runtime. Pair with + * `focusable = false` for passive overlays (watermarks, HUDs) that must + * never intercept input. + */ + clickThrough: Boolean = false, + /** + * Show the window on every desktop rather than only the one it was created + * on (macOS Spaces, Linux workspaces, Windows virtual desktops). Reactive. + * + * macOS `NSWindowCollectionBehaviorCanJoinAllSpaces` / Linux + * `gtk_window_stick()`; no-op on Windows, where a [hiddenFromDock] window is + * already visible on all desktops (`WS_EX_TOOLWINDOW` windows are not + * tracked by the Virtual Desktop Manager). Without it a macOS overlay + * disappears as soon as the user switches Space. + * + * See [TaoWindow.setVisibleOnAllWorkspaces] for the full-screen Space + * caveat. + */ + visibleOnAllWorkspaces: Boolean = false, content: @Composable TaoDecoratedWindowScope.() -> Unit, ) { val latestOnClose by rememberUpdatedState(onCloseRequest) @@ -425,6 +439,9 @@ public fun ApplicationScope.DecoratedWindow( LaunchedEffect(window, alwaysOnTop) { window.setAlwaysOnTop(alwaysOnTop) } LaunchedEffect(window, focusable) { window.setFocusable(focusable) } LaunchedEffect(window, clickThrough) { window.setIgnoreCursorEvents(clickThrough) } + LaunchedEffect(window, visibleOnAllWorkspaces) { + window.setVisibleOnAllWorkspaces(visibleOnAllWorkspaces) + } LaunchedEffect(window, visible) { if (visible) { window.show() diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt index 5969be61f..951333862 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt @@ -648,6 +648,25 @@ public class TaoWindow internal constructor( NativeTaoBridge.nativeSetIgnoreCursorEvents(handle, ignore) } + /** + * Shows the window on every desktop instead of only the one it was created + * on — macOS `NSWindowCollectionBehaviorCanJoinAllSpaces`, Linux + * `gtk_window_stick()` (X11/XWayland; native Wayland has no such + * protocol). + * + * No-op on Windows, which needs none: a window excluded from the taskbar + * (`hiddenFromDock`, i.e. `WS_EX_TOOLWINDOW`) is not tracked by the Virtual + * Desktop Manager and is already visible on all desktops. + * + * macOS caveat: this joins every regular Space. Floating above *another* + * app's full-screen Space additionally needs a window level above + * `NSFloatingWindowLevel` (what [setAlwaysOnTop] maps to), which Tao does + * not expose. + */ + public fun setVisibleOnAllWorkspaces(visible: Boolean) { + NativeTaoBridge.nativeSetVisibleOnAllWorkspaces(handle, visible) + } + /** Logical pixels. Pass `null` to clear the minimum. */ public fun setMinimumSize( widthDp: Double?, diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt index 231fee285..d2e217cb1 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt @@ -446,6 +446,12 @@ internal object NativeTaoBridge { ignore: Boolean, ) + @JvmStatic + external fun nativeSetVisibleOnAllWorkspaces( + handle: Long, + visible: Boolean, + ) + /** * Raises the window to the top of the z-order, restores it if minimized, * and gives it keyboard focus. Maps to Tao's `Window::set_focus()` which diff --git a/decorated-window-tao/src/main/native/src/event_loop.rs b/decorated-window-tao/src/main/native/src/event_loop.rs index 283fd1b82..637a8ab65 100644 --- a/decorated-window-tao/src/main/native/src/event_loop.rs +++ b/decorated-window-tao/src/main/native/src/event_loop.rs @@ -498,6 +498,21 @@ pub(crate) fn run_event_loop_blocking() { } } } + UserEvent::SetVisibleOnAllWorkspaces { handle, visible } => { + // macOS: NSWindowCollectionBehaviorCanJoinAllSpaces — an + // NSWindow otherwise stays bound to the Space it was created + // in, so an overlay vanishes the moment the user switches + // desktop. Linux: gtk_window_stick(). Windows: tao no-op, + // and none is needed — a taskbar-excluded (WS_EX_TOOLWINDOW) + // window is not tracked by the Virtual Desktop Manager and + // therefore already shows on every desktop. + let guard = WINDOWS.lock().unwrap(); + if let Some(map) = guard.as_ref() { + if let Some(w) = map.get(&handle) { + w.set_visible_on_all_workspaces(visible); + } + } + } UserEvent::Focus { handle } => { let guard = WINDOWS.lock().unwrap(); if let Some(map) = guard.as_ref() { diff --git a/decorated-window-tao/src/main/native/src/events.rs b/decorated-window-tao/src/main/native/src/events.rs index a707c830b..e8c69f2f8 100644 --- a/decorated-window-tao/src/main/native/src/events.rs +++ b/decorated-window-tao/src/main/native/src/events.rs @@ -326,6 +326,10 @@ pub(crate) enum UserEvent { handle: u64, ignore: bool, }, + SetVisibleOnAllWorkspaces { + handle: u64, + visible: bool, + }, Focus { handle: u64, }, diff --git a/decorated-window-tao/src/main/native/src/window_jni.rs b/decorated-window-tao/src/main/native/src/window_jni.rs index 72f27179e..9aa15f8f6 100644 --- a/decorated-window-tao/src/main/native/src/window_jni.rs +++ b/decorated-window-tao/src/main/native/src/window_jni.rs @@ -398,6 +398,19 @@ pub extern "system" fn Java_dev_nucleusframework_window_tao_ffi_NativeTaoBridge_ }); } +#[no_mangle] +pub extern "system" fn Java_dev_nucleusframework_window_tao_ffi_NativeTaoBridge_nativeSetVisibleOnAllWorkspaces( + _env: JNIEnv, + _class: JClass, + handle: jlong, + visible: jboolean, +) { + send_user_event(UserEvent::SetVisibleOnAllWorkspaces { + handle: handle as u64, + visible: visible != JNI_FALSE, + }); +} + #[no_mangle] pub extern "system" fn Java_dev_nucleusframework_window_tao_ffi_NativeTaoBridge_nativeSetMinInnerSize( _env: JNIEnv, diff --git a/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt index 4ad42c6aa..0fda609ee 100644 --- a/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt +++ b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt @@ -46,9 +46,11 @@ import java.io.File * watermark pill — everything else shows the desktop through. * * The app has no Dock/taskbar presence (`hiddenFromDock`), never takes focus - * (`focusable = false`) and lets every click fall through to whatever sits - * underneath (`clickThrough`); it is controlled entirely from its tray icon - * (ComposeNativeTray): pick the corner the watermark is pinned to, or quit. + * (`focusable = false`), lets every click fall through to whatever sits + * underneath (`clickThrough`) and stays on screen across desktops / + * macOS Spaces (`visibleOnAllWorkspaces`); it is controlled entirely from its + * tray icon (ComposeNativeTray): pick the corner the watermark is pinned to, + * or quit. * Corner changes go through `WindowState.position = WindowPosition.Aligned(...)`, * which the Tao backend resolves against the primary monitor's work area at * runtime. @@ -112,10 +114,14 @@ fun main(args: Array) = undecorated = true, transparent = true, hiddenFromDock = true, + focusable = false, // A watermark must never get in the way: clicks fall through to // whatever is underneath and the window never takes focus. clickThrough = true, - focusable = false, + // …and it follows the user across desktops. macOS/Linux need this + // explicitly; on Windows a taskbar-excluded window already shows on + // every virtual desktop. + visibleOnAllWorkspaces = true, ) { AnimatedWatermark() } diff --git a/nucleus-application/api/nucleus-application.api b/nucleus-application/api/nucleus-application.api index 662f27727..e0ba36642 100644 --- a/nucleus-application/api/nucleus-application.api +++ b/nucleus-application/api/nucleus-application.api @@ -9,8 +9,8 @@ public final class dev/nucleusframework/application/DecoratedDialogKt { } public final class dev/nucleusframework/application/DecoratedWindowKt { - public static final fun DecoratedWindow-O3YsXe4 (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V - public static final fun DecoratedWindow-OCZPU9E (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun DecoratedWindow-Uc0goEM (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun DecoratedWindow-XZmG138 (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class dev/nucleusframework/application/DefaultNucleusDialogHost : dev/nucleusframework/application/NucleusDialogHost { diff --git a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt index 32230e273..2991d744a 100644 --- a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt +++ b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt @@ -33,16 +33,6 @@ public fun NucleusApplicationScope.DecoratedWindow( // Fully borderless window (no macOS traffic lights) — for overlay/ghost windows. // Honoured by the Tao backend; the AWT backend currently ignores it. undecorated: Boolean = false, - // Full-window per-pixel transparency: pixels the content leaves at alpha 0 - // show the desktop behind the window (#416). Creation-time only — cannot - // change after the native window exists. Typically combined with - // [undecorated]. Honoured by the Tao backend; the AWT backend ignores it. - transparent: Boolean = false, - // Click-through window: pointer events fall through to whatever sits - // below, and the window never intercepts input. Pair with - // `focusable = false` for passive overlays (watermarks, HUDs). Reactive. - // Honoured by the Tao backend; the AWT backend ignores it. - clickThrough: Boolean = false, // Linux/Tao only: make this window a popup overlay of [popupFor]. On // Wayland it maps as a wl_subsurface of the parent — the only window kind // a client can freely position under xdg-shell (coordinates are @@ -68,6 +58,26 @@ public fun NucleusApplicationScope.DecoratedWindow( minimumSize: DpSize? = null, onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, + // The overlay flags below are appended rather than grouped with + // [undecorated]: inserting a parameter mid-list silently shifts every + // positional call site. Keep new parameters here, before [content]. + // + // Full-window per-pixel transparency: pixels the content leaves at alpha 0 + // show the desktop behind the window (#416). Creation-time only — cannot + // change after the native window exists. Typically combined with + // [undecorated]. Honoured by the Tao backend; the AWT backend ignores it. + transparent: Boolean = false, + // Click-through window: pointer events fall through to whatever sits + // below, and the window never intercepts input. Pair with + // `focusable = false` for passive overlays (watermarks, HUDs). Reactive. + // Honoured by the Tao backend; the AWT backend ignores it. + clickThrough: Boolean = false, + // Show the window on every desktop instead of only the one it was created + // on — macOS Spaces (`NSWindowCollectionBehaviorCanJoinAllSpaces`), Linux + // workspaces (`gtk_window_stick`, X11/XWayland). No-op on Windows, where a + // [hiddenFromDock] window already shows on every virtual desktop. Reactive. + // Honoured by the Tao backend; the AWT backend ignores it. + visibleOnAllWorkspaces: Boolean = false, content: @Composable NucleusDecoratedWindowScope.() -> Unit, ) { when (this) { @@ -119,6 +129,7 @@ public fun NucleusApplicationScope.DecoratedWindow( undecorated = undecorated, transparent = transparent, clickThrough = clickThrough, + visibleOnAllWorkspaces = visibleOnAllWorkspaces, popupFor = popupFor, nativePopupLayers = nativePopupLayers, nativeContextMenu = nativeContextMenu, @@ -154,8 +165,6 @@ public fun DecoratedWindow( focusable: Boolean = true, alwaysOnTop: Boolean = false, undecorated: Boolean = false, - transparent: Boolean = false, - clickThrough: Boolean = false, popupFor: NucleusWindow? = null, nativePopupLayers: Boolean = false, nativeContextMenu: Boolean = false, @@ -163,6 +172,9 @@ public fun DecoratedWindow( minimumSize: DpSize? = null, onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, + transparent: Boolean = false, + clickThrough: Boolean = false, + visibleOnAllWorkspaces: Boolean = false, content: @Composable NucleusDecoratedWindowScope.() -> Unit, ) { LocalNucleusApplicationScope.current.DecoratedWindow( @@ -176,8 +188,6 @@ public fun DecoratedWindow( focusable = focusable, alwaysOnTop = alwaysOnTop, undecorated = undecorated, - transparent = transparent, - clickThrough = clickThrough, popupFor = popupFor, nativePopupLayers = nativePopupLayers, nativeContextMenu = nativeContextMenu, @@ -185,6 +195,9 @@ public fun DecoratedWindow( minimumSize = minimumSize, onPreviewKeyEvent = onPreviewKeyEvent, onKeyEvent = onKeyEvent, + transparent = transparent, + clickThrough = clickThrough, + visibleOnAllWorkspaces = visibleOnAllWorkspaces, content = content, ) } diff --git a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt index 0e801089a..71bab4c3f 100644 --- a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt +++ b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt @@ -50,6 +50,7 @@ internal object TaoDecoratedWindowAdapter { undecorated: Boolean, transparent: Boolean, clickThrough: Boolean, + visibleOnAllWorkspaces: Boolean, popupFor: NucleusWindow?, nativePopupLayers: Boolean, nativeContextMenu: Boolean, @@ -92,6 +93,7 @@ internal object TaoDecoratedWindowAdapter { undecorated = undecorated, transparent = transparent, clickThrough = clickThrough, + visibleOnAllWorkspaces = visibleOnAllWorkspaces, popupFor = popupFor?.unsafe?.taoWindow, nativePopupLayers = nativePopupLayers, hiddenFromDock = hiddenFromDock, From d1aa1d3a98652fe0be9e89e4c46ecc313eda6755 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Mon, 17 Aug 2026 18:52:46 +0300 Subject: [PATCH 06/12] fix(tao): make the Linux click-through input region empty and reversible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vendored tao patch 0006. `CursorIgnoreEvents` installed a 1x1 rectangle at the origin instead of an empty region, so the top-left pixel of a click-through window stayed clickable, and it cleared the shape on the GtkWidget while installing it on the GdkWindow — which never undid it, so click-through could not be switched back off. Both branches now go through the same GdkWindow; the clear passes a NULL region (the only form that survives a later resize, and one the safe binding cannot express). --- ...06-linux-cursor-ignore-events-region.patch | 42 +++++++++++++++++++ .../main/native/vendor/tao-patches/README.md | 1 + .../tao/src/platform_impl/linux/event_loop.rs | 33 +++++++++++---- 3 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 decorated-window-tao/src/main/native/vendor/tao-patches/0006-linux-cursor-ignore-events-region.patch diff --git a/decorated-window-tao/src/main/native/vendor/tao-patches/0006-linux-cursor-ignore-events-region.patch b/decorated-window-tao/src/main/native/vendor/tao-patches/0006-linux-cursor-ignore-events-region.patch new file mode 100644 index 000000000..d9f0c7df9 --- /dev/null +++ b/decorated-window-tao/src/main/native/vendor/tao-patches/0006-linux-cursor-ignore-events-region.patch @@ -0,0 +1,42 @@ +--- a/src/platform_impl/linux/event_loop.rs ++++ b/src/platform_impl/linux/event_loop.rs +@@ -495,15 +495,30 @@ impl EventLoop { + } + } + WindowRequest::CursorIgnoreEvents(ignore) => { +- if ignore { +- let empty_region = Region::create_rectangle(&RectangleInt::new(0, 0, 1, 1)); +- window +- .window() +- .unwrap() +- .input_shape_combine_region(&empty_region, 0, 0); +- } else { +- window.input_shape_combine_region(None) +- }; ++ // PATCH(nucleus): an *empty* region, not a 1x1 rectangle at the ++ // origin — upstream leaves the top-left pixel clickable. Both ++ // branches also go through the same GdkWindow: upstream cleared ++ // the shape on the GtkWidget, which is a no-op when the shape was ++ // installed on the GdkWindow, so click-through could never be ++ // turned back off. ++ if let Some(gdk_window) = window.window() { ++ if ignore { ++ gdk_window.input_shape_combine_region(&Region::create(), 0, 0); ++ } else { ++ // Only a NULL region clears the shape for good; a full-window ++ // region would go stale on the next resize, and the safe ++ // binding cannot express NULL. ++ use glib::translate::ToGlibPtr; ++ unsafe { ++ gdk::ffi::gdk_window_input_shape_combine_region( ++ gdk_window.to_glib_none().0, ++ std::ptr::null_mut(), ++ 0, ++ 0, ++ ); ++ } ++ } ++ } + } + WindowRequest::ProgressBarState(_) => unreachable!(), + WindowRequest::BadgeCount(_, _) => unreachable!(), diff --git a/decorated-window-tao/src/main/native/vendor/tao-patches/README.md b/decorated-window-tao/src/main/native/vendor/tao-patches/README.md index 2c9d3cdd5..a5b95a8cf 100644 --- a/decorated-window-tao/src/main/native/vendor/tao-patches/README.md +++ b/decorated-window-tao/src/main/native/vendor/tao-patches/README.md @@ -19,6 +19,7 @@ Tao 0.35.0 is already vendored; this file is the living list of patches. | 0003 | `0003-linux-realize-on-build.patch` | 3 | Linux | Realise the GtkApplicationWindow at the end of `Window::new` so the underlying GdkWindow (X11 XID / Wayland wl_surface) is valid synchronously when the constructor returns. | | 0004 | `0004-linux-drain-draw-queue.patch` | 4 | Linux | `run_return`: treat pending redraws like pending events (don't park in the blocking `gtk_main_iteration` while `draws` is non-empty) and drain the whole draw channel per cycle instead of one redraw per wakeup. Fixes multi-window frame starvation (each window rendered at ~refresh/N). | | 0005 | `0005-linux-restore-activation-timestamp.patch` | 5 | Linux | Stamp `Focus` and `Minimized(false)` activations with a real X server timestamp (`gdk_x11_get_server_time`). Mutter's focus-stealing prevention drops `_NET_ACTIVE_WINDOW` requests carrying `GDK_CURRENT_TIME` (0) and keeps a deiconified window Iconic with `_NET_WM_STATE_DEMANDS_ATTENTION`, so restore/focus silently no-op and `EVENT_MINIMIZED(false)` never fires on GNOME X11/XWayland (openbox honors the 0 timestamp, which is why CI never saw it). No-op on Wayland. | +| 0006 | `0006-linux-cursor-ignore-events-region.patch` | 6 | Linux | `CursorIgnoreEvents`: install a genuinely *empty* input region instead of upstream's 1x1 rectangle at the origin (which leaves the top-left pixel clickable), and clear it through the same `GdkWindow` with a NULL region. Upstream cleared it on the `GtkWidget`, which never undid a shape installed on the `GdkWindow`, so click-through could not be switched back off. | ## Bump procedure (e.g. 0.35 → 0.36) diff --git a/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/linux/event_loop.rs b/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/linux/event_loop.rs index 0a789a3ce..7d9501503 100644 --- a/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/linux/event_loop.rs +++ b/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/linux/event_loop.rs @@ -495,15 +495,30 @@ impl EventLoop { } } WindowRequest::CursorIgnoreEvents(ignore) => { - if ignore { - let empty_region = Region::create_rectangle(&RectangleInt::new(0, 0, 1, 1)); - window - .window() - .unwrap() - .input_shape_combine_region(&empty_region, 0, 0); - } else { - window.input_shape_combine_region(None) - }; + // PATCH(nucleus): an *empty* region, not a 1x1 rectangle at the + // origin — upstream leaves the top-left pixel clickable. Both + // branches also go through the same GdkWindow: upstream cleared + // the shape on the GtkWidget, which is a no-op when the shape was + // installed on the GdkWindow, so click-through could never be + // turned back off. + if let Some(gdk_window) = window.window() { + if ignore { + gdk_window.input_shape_combine_region(&Region::create(), 0, 0); + } else { + // Only a NULL region clears the shape for good; a full-window + // region would go stale on the next resize, and the safe + // binding cannot express NULL. + use glib::translate::ToGlibPtr; + unsafe { + gdk::ffi::gdk_window_input_shape_combine_region( + gdk_window.to_glib_none().0, + std::ptr::null_mut(), + 0, + 0, + ); + } + } + } } WindowRequest::ProgressBarState(_) => unreachable!(), WindowRequest::BadgeCount(_, _) => unreachable!(), From 8bc9fce2a6523ac261336e704b84c3da623cce86 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Mon, 17 Aug 2026 18:52:47 +0300 Subject: [PATCH 07/12] fix(tao): replay click-through after show so it survives the first map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `clickThrough` was a no-op on Linux: the LaunchedEffect applies it at the first composition, but every DecoratedWindow is created hidden and shown only after the first paint, and the GDK input region is bound to the GdkWindow alive when it is installed — the window swaps it on the way to its first map, taking the region with it. Measured on GNOME/XWayland: the window kept a full-size ShapeInput rectangle and swallowed every click, while the same call made after the window was mapped landed correctly. TaoWindow now remembers the requested state and replays it from show(), queued behind the show on the same event loop. --- .../dev/nucleusframework/window/tao/TaoWindow.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt index 951333862..683a64acc 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt @@ -645,9 +645,21 @@ public class TaoWindow internal constructor( * watermarks or HUDs that must never intercept input. */ public fun setIgnoreCursorEvents(ignore: Boolean) { + ignoreCursorEvents = ignore NativeTaoBridge.nativeSetIgnoreCursorEvents(handle, ignore) } + /** + * Last requested [setIgnoreCursorEvents] state, replayed by [show] — on + * Linux the GDK input region is bound to the `GdkWindow` alive when it is + * installed, and a window created hidden (every [DecoratedWindow], which + * shows only after the first paint) swaps it on the way to its first map. + * Windows and macOS keep the flag across the show, but replaying is a + * plain style/flag write there too, so it stays unconditional. + */ + @Volatile + private var ignoreCursorEvents: Boolean = false + /** * Shows the window on every desktop instead of only the one it was created * on — macOS `NSWindowCollectionBehaviorCanJoinAllSpaces`, Linux @@ -755,6 +767,10 @@ public class TaoWindow internal constructor( startupEraseActive = true setStartupBackgroundEraseEnabled(true) NativeTaoBridge.nativeSetVisible(handle, true) + // Queued behind the show above (both ride the same event loop), so the + // click-through state lands on the mapped window — see + // [ignoreCursorEvents]. + if (ignoreCursorEvents) NativeTaoBridge.nativeSetIgnoreCursorEvents(handle, true) } public fun hide() { From 5bd741005aa5494d77e4be2e94b7a71342ee430e Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Mon, 17 Aug 2026 18:58:35 +0300 Subject: [PATCH 08/12] feat(tao): warn when a window-management feature is a native Wayland no-op MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit alwaysOnTop and visibleOnAllWorkspaces silently did nothing on native Wayland — the protocol has neither client-side stacking nor workspaces, so gtk_window_set_keep_above and gtk_window_stick are ignored. They now log once per window, mirroring what hiddenFromDock already did, and point at NUCLEUS_TAO_LINUX_RENDERER=x11. The check reads the surface kind the native side reports for that window rather than sniffing the environment, so it stays correct per window. --- .../window/tao/DecoratedWindowComposable.kt | 6 ++- .../nucleusframework/window/tao/TaoWindow.kt | 45 +++++++++++++++++++ .../main/kotlin/com/example/watermark/Main.kt | 7 +++ .../application/DecoratedWindow.kt | 3 +- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt index 8cfebb235..9e520dcd9 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt @@ -144,8 +144,10 @@ public fun ApplicationScope.DecoratedWindow( * on (macOS Spaces, Linux workspaces, Windows virtual desktops). Reactive. * * macOS `NSWindowCollectionBehaviorCanJoinAllSpaces` / Linux - * `gtk_window_stick()`; no-op on Windows, where a [hiddenFromDock] window is - * already visible on all desktops (`WS_EX_TOOLWINDOW` windows are not + * `gtk_window_stick()` (X11 and XWayland only — native Wayland has no + * workspace protocol and logs a warning); no-op on Windows, where a + * [hiddenFromDock] window is already visible on all desktops + * (`WS_EX_TOOLWINDOW` windows are not * tracked by the Virtual Desktop Manager). Without it a macOS overlay * disappears as soon as the user switches Space. * diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt index 683a64acc..cee4167f0 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt @@ -9,8 +9,10 @@ import dev.nucleusframework.window.tao.ffi.NativeTaoBridge import dev.nucleusframework.window.tao.ffi.NativeTaoLinuxTouchBridge import dev.nucleusframework.window.tao.ffi.NativeTaoMacOsDecoBridge import dev.nucleusframework.window.tao.ffi.NativeTaoWindowsDecoBridge +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean +import java.util.logging.Logger /** * Phase 2 handle to a window owned by the Tao event loop. @@ -630,6 +632,13 @@ public class TaoWindow internal constructor( } public fun setAlwaysOnTop(alwaysOnTop: Boolean) { + if (alwaysOnTop) { + warnIfNativeWayland( + "alwaysOnTop", + "Wayland has no client-side stacking protocol (xdg-shell exposes none and " + + "Mutter rejects wlr-layer-shell), so gtk_window_set_keep_above is ignored.", + ) + } NativeTaoBridge.nativeSetAlwaysOnTop(handle, alwaysOnTop) } @@ -676,9 +685,43 @@ public class TaoWindow internal constructor( * not expose. */ public fun setVisibleOnAllWorkspaces(visible: Boolean) { + if (visible) { + warnIfNativeWayland( + "visibleOnAllWorkspaces", + "Wayland has no client-side workspace protocol, so gtk_window_stick is ignored.", + ) + } NativeTaoBridge.nativeSetVisibleOnAllWorkspaces(handle, visible) } + /** + * Logs once per window and per [feature] when this window's surface is a + * native Wayland one, where several window-management features are simply + * absent from the protocol. The check is per-window (the surface kind + * reported by the native side), not a process-wide env sniff. + */ + private fun warnIfNativeWayland( + feature: String, + detail: String, + ) { + if (!isNativeWaylandSurface || !waylandWarnings.add(feature)) return + waylandLogger.warning( + "$feature has no effect on native Wayland: $detail " + + "Run with NUCLEUS_TAO_LINUX_RENDERER=x11 (XWayland) if the app needs it.", + ) + } + + /** `true` when this window is backed by a native Wayland surface. */ + private val isNativeWaylandSurface: Boolean + get() { + if (Platform.Current != Platform.Linux || !NativeTaoBridge.isLoaded) return false + val handles = NativeTaoBridge.nativeLinuxHandles(handle) ?: return false + return handles.isNotEmpty() && handles[0] == WAYLAND_HANDLE_KIND + } + + /** Features already reported through [warnIfNativeWayland] for this window. */ + private val waylandWarnings = ConcurrentHashMap.newKeySet() + /** Logical pixels. Pass `null` to clear the minimum. */ public fun setMinimumSize( widthDp: Double?, @@ -1066,6 +1109,8 @@ public class TaoWindow internal constructor( /** `nativeLinuxHandles` slot 0: 1 = Xlib, 2 = Wayland. */ const val WAYLAND_HANDLE_KIND: Long = 2L + + val waylandLogger: Logger = Logger.getLogger("dev.nucleusframework.window.tao.wayland") } } diff --git a/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt index 0fda609ee..9077c200d 100644 --- a/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt +++ b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt @@ -55,6 +55,13 @@ import java.io.File * which the Tao backend resolves against the primary monitor's work area at * runtime. * + * **Linux**: an overlay like this needs X11 semantics — stacking + * (`alwaysOnTop`), programmatic positioning (`WindowPosition.Aligned`) and + * workspace stickiness are all absent from the Wayland protocol, and the Tao + * backend logs a warning for each. Run under XWayland + * (`NUCLEUS_TAO_LINUX_RENDERER=x11`) or give the overlay window its own X11 + * surface while the rest of the app stays on Wayland. + * * The tray icon is fed as image *files* (the `iconPath` overload): the * composable-icon overloads render through a skiko `Image.encodeToData` * signature that no longer exists in the skiko shipped with Compose 1.12, and diff --git a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt index 2991d744a..2c731648a 100644 --- a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt +++ b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt @@ -74,7 +74,8 @@ public fun NucleusApplicationScope.DecoratedWindow( clickThrough: Boolean = false, // Show the window on every desktop instead of only the one it was created // on — macOS Spaces (`NSWindowCollectionBehaviorCanJoinAllSpaces`), Linux - // workspaces (`gtk_window_stick`, X11/XWayland). No-op on Windows, where a + // workspaces (`gtk_window_stick`, X11/XWayland only — native Wayland has no + // workspace protocol and logs a warning). No-op on Windows, where a // [hiddenFromDock] window already shows on every virtual desktop. Reactive. // Honoured by the Tao backend; the AWT backend ignores it. visibleOnAllWorkspaces: Boolean = false, From 446c3237bea39374ced88cd8914fc0f278fd587b Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Mon, 17 Aug 2026 19:18:48 +0300 Subject: [PATCH 09/12] feat(tao): let a single window take an X11 surface on a Wayland session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wayland has no protocol for client-side stacking, programmatic positioning or workspace stickiness, so an overlay that needs them had only one way out: NUCLEUS_TAO_LINUX_RENDERER=x11, which drags the entire app onto XWayland. GTK drives several GdkDisplays from one process and one main loop, so `forceX11` opens the X server named by DISPLAY once and re-homes just that window's GdkWindow onto it. The rest of the app keeps its Wayland surfaces. Reported per window through the new TaoWindow.isNativeWaylandSurface, which reads the live surface instead of sniffing the environment, so a window that could not be re-homed (no XWayland) says so and logs a warning. Exposed on the Tao and backend-agnostic DecoratedWindow, and — together with transparent / clickThrough / visibleOnAllWorkspaces — on the Material 2, Material 3 and Jewel wrappers, which had no way to reach any of them. The Material wrappers also gain `undecorated`, without which `transparent` has no borderless window to sit on. --- .../window/jewel/JewelDecoratedWindow.kt | 21 +++++++ .../api/decorated-window-material2.api | 2 +- .../material2/MaterialDecoratedWindow.kt | 25 ++++++++ .../api/decorated-window-material3.api | 2 +- .../material/MaterialDecoratedWindow.kt | 25 ++++++++ .../api/decorated-window-tao.api | 7 ++- .../window/tao/DecoratedWindow.kt | 10 ++- .../window/tao/DecoratedWindowComposable.kt | 15 +++++ .../window/tao/TaoApplication.kt | 6 ++ .../nucleusframework/window/tao/TaoWindow.kt | 27 +++++++- .../window/tao/ffi/NativeTaoBridge.kt | 7 +++ .../src/main/native/src/event_loop.rs | 63 +++++++++++++++++++ .../src/main/native/src/events.rs | 6 ++ .../src/main/native/src/window_jni.rs | 2 + ...06-linux-cursor-ignore-events-region.patch | 49 +++------------ .../tao/src/platform_impl/linux/event_loop.rs | 2 +- .../rect-stress-demo/api/rect-stress-demo.api | 12 ++++ .../api/nucleus-application.api | 4 +- .../application/DecoratedWindow.kt | 11 ++++ .../internal/TaoDecoratedWindowAdapter.kt | 2 + 20 files changed, 246 insertions(+), 52 deletions(-) create mode 100644 examples/rect-stress-demo/api/rect-stress-demo.api diff --git a/decorated-window-jewel/src/main/kotlin/dev/nucleusframework/window/jewel/JewelDecoratedWindow.kt b/decorated-window-jewel/src/main/kotlin/dev/nucleusframework/window/jewel/JewelDecoratedWindow.kt index dda1b8d5e..b829979e0 100644 --- a/decorated-window-jewel/src/main/kotlin/dev/nucleusframework/window/jewel/JewelDecoratedWindow.kt +++ b/decorated-window-jewel/src/main/kotlin/dev/nucleusframework/window/jewel/JewelDecoratedWindow.kt @@ -111,6 +111,23 @@ public fun NucleusApplicationScope.JewelDecoratedWindow( onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, titleBarStyle: TitleBarStyle? = null, + // The overlay flags below mirror `dev.nucleusframework.application.DecoratedWindow`. + // + // Full-window per-pixel transparency: pixels the content leaves at alpha 0 + // show the desktop behind the window. Creation-time only, normally paired + // with [undecorated]. Tao backend only. + transparent: Boolean = false, + // Click-through window: pointer events fall through to whatever sits below + // and the window never intercepts input. Pair with `focusable = false` for + // passive overlays. Reactive. Tao backend only. + clickThrough: Boolean = false, + // Show the window on every desktop / macOS Space / Windows virtual desktop + // instead of only the one it was created on. Reactive. Tao backend only. + visibleOnAllWorkspaces: Boolean = false, + // Linux only: give this window an X11 surface even when the app runs on a + // native Wayland session, for the window management Wayland has no protocol + // for (stacking, positioning, workspace stickiness). Creation-time only. + forceX11: Boolean = false, content: @Composable NucleusDecoratedWindowScope.() -> Unit, ) { val windowStyle = rememberJewelWindowStyle() @@ -133,6 +150,10 @@ public fun NucleusApplicationScope.JewelDecoratedWindow( enabled = enabled, focusable = focusable, alwaysOnTop = alwaysOnTop, + transparent = transparent, + clickThrough = clickThrough, + visibleOnAllWorkspaces = visibleOnAllWorkspaces, + forceX11 = forceX11, undecorated = undecorated, popupFor = popupFor, nativeContextMenu = nativeContextMenu, diff --git a/decorated-window-material2/api/decorated-window-material2.api b/decorated-window-material2/api/decorated-window-material2.api index 2202418df..0bd04a37d 100644 --- a/decorated-window-material2/api/decorated-window-material2.api +++ b/decorated-window-material2/api/decorated-window-material2.api @@ -17,7 +17,7 @@ public final class dev/nucleusframework/window/material2/MaterialDecoratedDialog public final class dev/nucleusframework/window/material2/MaterialDecoratedWindowKt { public static final fun MaterialDecoratedWindow-CL87EUo (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ldev/nucleusframework/window/styling/TitleBarStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V - public static final fun MaterialDecoratedWindow-fsutTYA (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ldev/nucleusframework/window/styling/TitleBarStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun MaterialDecoratedWindow-On4RJk0 (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ldev/nucleusframework/window/styling/TitleBarStyle;ZZZZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class dev/nucleusframework/window/material2/MaterialDialogTitleBarKt { diff --git a/decorated-window-material2/src/main/kotlin/dev/nucleusframework/window/material2/MaterialDecoratedWindow.kt b/decorated-window-material2/src/main/kotlin/dev/nucleusframework/window/material2/MaterialDecoratedWindow.kt index 5bdc8e2aa..79c7886c9 100644 --- a/decorated-window-material2/src/main/kotlin/dev/nucleusframework/window/material2/MaterialDecoratedWindow.kt +++ b/decorated-window-material2/src/main/kotlin/dev/nucleusframework/window/material2/MaterialDecoratedWindow.kt @@ -93,6 +93,26 @@ public fun NucleusApplicationScope.MaterialDecoratedWindow( onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, titleBarStyle: TitleBarStyle? = null, + // Fully borderless window (no macOS traffic lights, no CSD outline) — for + // overlay/ghost windows. Tao backend only. + undecorated: Boolean = false, + // The overlay flags below mirror `dev.nucleusframework.application.DecoratedWindow`. + // + // Full-window per-pixel transparency: pixels the content leaves at alpha 0 + // show the desktop behind the window. Creation-time only, normally paired + // with [undecorated]. Tao backend only. + transparent: Boolean = false, + // Click-through window: pointer events fall through to whatever sits below + // and the window never intercepts input. Pair with `focusable = false` for + // passive overlays. Reactive. Tao backend only. + clickThrough: Boolean = false, + // Show the window on every desktop / macOS Space / Windows virtual desktop + // instead of only the one it was created on. Reactive. Tao backend only. + visibleOnAllWorkspaces: Boolean = false, + // Linux only: give this window an X11 surface even when the app runs on a + // native Wayland session, for the window management Wayland has no protocol + // for (stacking, positioning, workspace stickiness). Creation-time only. + forceX11: Boolean = false, content: @Composable NucleusDecoratedWindowScope.() -> Unit, ) { val outerColors = MaterialTheme.colors @@ -117,6 +137,11 @@ public fun NucleusApplicationScope.MaterialDecoratedWindow( enabled = enabled, focusable = focusable, alwaysOnTop = alwaysOnTop, + undecorated = undecorated, + transparent = transparent, + clickThrough = clickThrough, + visibleOnAllWorkspaces = visibleOnAllWorkspaces, + forceX11 = forceX11, nativePopupLayers = nativePopupLayers, nativeContextMenu = nativeContextMenu, hiddenFromDock = hiddenFromDock, diff --git a/decorated-window-material3/api/decorated-window-material3.api b/decorated-window-material3/api/decorated-window-material3.api index 0db801a26..7efc8bbed 100644 --- a/decorated-window-material3/api/decorated-window-material3.api +++ b/decorated-window-material3/api/decorated-window-material3.api @@ -23,7 +23,7 @@ public final class dev/nucleusframework/window/material/MaterialDecoratedDialogK public final class dev/nucleusframework/window/material/MaterialDecoratedWindowKt { public static final fun MaterialDecoratedWindow-2nA36Wk (Landroidx/compose/ui/window/ApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ldev/nucleusframework/window/styling/TitleBarStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V - public static final fun MaterialDecoratedWindow-fsutTYA (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ldev/nucleusframework/window/styling/TitleBarStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun MaterialDecoratedWindow-On4RJk0 (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ldev/nucleusframework/window/styling/TitleBarStyle;ZZZZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class dev/nucleusframework/window/material/MaterialDialogTitleBarKt { diff --git a/decorated-window-material3/src/main/kotlin/dev/nucleusframework/window/material/MaterialDecoratedWindow.kt b/decorated-window-material3/src/main/kotlin/dev/nucleusframework/window/material/MaterialDecoratedWindow.kt index 5a96b330c..4db44e62c 100644 --- a/decorated-window-material3/src/main/kotlin/dev/nucleusframework/window/material/MaterialDecoratedWindow.kt +++ b/decorated-window-material3/src/main/kotlin/dev/nucleusframework/window/material/MaterialDecoratedWindow.kt @@ -112,6 +112,26 @@ public fun NucleusApplicationScope.MaterialDecoratedWindow( onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, titleBarStyle: TitleBarStyle? = null, + // Fully borderless window (no macOS traffic lights, no CSD outline) — for + // overlay/ghost windows. Tao backend only. + undecorated: Boolean = false, + // The overlay flags below mirror `dev.nucleusframework.application.DecoratedWindow`. + // + // Full-window per-pixel transparency: pixels the content leaves at alpha 0 + // show the desktop behind the window. Creation-time only, normally paired + // with [undecorated]. Tao backend only. + transparent: Boolean = false, + // Click-through window: pointer events fall through to whatever sits below + // and the window never intercepts input. Pair with `focusable = false` for + // passive overlays. Reactive. Tao backend only. + clickThrough: Boolean = false, + // Show the window on every desktop / macOS Space / Windows virtual desktop + // instead of only the one it was created on. Reactive. Tao backend only. + visibleOnAllWorkspaces: Boolean = false, + // Linux only: give this window an X11 surface even when the app runs on a + // native Wayland session, for the window management Wayland has no protocol + // for (stacking, positioning, workspace stickiness). Creation-time only. + forceX11: Boolean = false, content: @Composable NucleusDecoratedWindowScope.() -> Unit, ) { val outerColorScheme = MaterialTheme.colorScheme @@ -136,6 +156,11 @@ public fun NucleusApplicationScope.MaterialDecoratedWindow( enabled = enabled, focusable = focusable, alwaysOnTop = alwaysOnTop, + undecorated = undecorated, + transparent = transparent, + clickThrough = clickThrough, + visibleOnAllWorkspaces = visibleOnAllWorkspaces, + forceX11 = forceX11, nativePopupLayers = nativePopupLayers, nativeContextMenu = nativeContextMenu, hiddenFromDock = hiddenFromDock, diff --git a/decorated-window-tao/api/decorated-window-tao.api b/decorated-window-tao/api/decorated-window-tao.api index 168d5737e..edc31d3ac 100644 --- a/decorated-window-tao/api/decorated-window-tao.api +++ b/decorated-window-tao/api/decorated-window-tao.api @@ -198,7 +198,7 @@ public final class dev/nucleusframework/window/tao/DecoratedDialogKt { } public final class dev/nucleusframework/window/tao/DecoratedWindowComposableKt { - public static final fun DecoratedWindow-krRzeA4 (Ldev/nucleusframework/window/tao/ApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;Ljava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/DpSize;ZZZZZZZZLdev/nucleusframework/window/tao/TaoWindow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLdev/nucleusframework/window/tao/MacOSStyle;ZLandroidx/compose/runtime/CompositionLocalContext;ZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun DecoratedWindow-119p4RY (Ldev/nucleusframework/window/tao/ApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;Ljava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/DpSize;ZZZZZZZZLdev/nucleusframework/window/tao/TaoWindow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLdev/nucleusframework/window/tao/MacOSStyle;ZLandroidx/compose/runtime/CompositionLocalContext;ZZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class dev/nucleusframework/window/tao/DecoratedWindowKt { @@ -500,8 +500,8 @@ public final class dev/nucleusframework/window/tao/TaoApplication { public static final field $stable I public static final field INSTANCE Ldev/nucleusframework/window/tao/TaoApplication; public final fun exit ()V - public final fun openWindow (Ljava/lang/String;DDZZZZLdev/nucleusframework/window/tao/TaoWindow;ZZZ)Ldev/nucleusframework/window/tao/TaoWindow; - public static synthetic fun openWindow$default (Ldev/nucleusframework/window/tao/TaoApplication;Ljava/lang/String;DDZZZZLdev/nucleusframework/window/tao/TaoWindow;ZZZILjava/lang/Object;)Ldev/nucleusframework/window/tao/TaoWindow; + public final fun openWindow (Ljava/lang/String;DDZZZZLdev/nucleusframework/window/tao/TaoWindow;ZZZZ)Ldev/nucleusframework/window/tao/TaoWindow; + public static synthetic fun openWindow$default (Ldev/nucleusframework/window/tao/TaoApplication;Ljava/lang/String;DDZZZZLdev/nucleusframework/window/tao/TaoWindow;ZZZZILjava/lang/Object;)Ldev/nucleusframework/window/tao/TaoWindow; public final fun run (Lkotlin/jvm/functions/Function1;)V } @@ -715,6 +715,7 @@ public final class dev/nucleusframework/window/tao/TaoWindow { public final fun isFullscreen ()Z public final fun isMaximized ()Z public final fun isMinimized ()Z + public final fun isNativeWaylandSurface ()Z public final fun isPopup ()Z public final fun isResizable ()Z public final fun isTiled ()Z diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt index e1c99daea..66f04a10e 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt @@ -261,6 +261,9 @@ internal fun ApplicationScope.openDecoratedWindow( // parent window's theme/user locals from the first composition without // hijacking popup positioning. See [LocalTaoCompositionLocalContextBridge]. initialCompositionLocalContext: CompositionLocalContext? = null, + // Linux only: give this window an X11 surface even on a native Wayland + // session — see DecoratedWindow(forceX11 = …). + forceX11: Boolean = false, content: @Composable TaoDecoratedWindowScope.() -> Unit, ): TaoWindow { // hiddenFromDock rides on the GTK skip-taskbar/skip-pager hint, which @@ -275,11 +278,13 @@ internal fun ApplicationScope.openDecoratedWindow( if (hiddenFromDock && Platform.Current == Platform.Linux && Platform.isWayland && - !forcesXWayland + !forcesXWayland && + !forceX11 ) { hiddenFromDockLogger.warning( "hiddenFromDock has no effect on native Wayland: Wayland has no client-side " + - "skip-taskbar protocol. Run with NUCLEUS_TAO_LINUX_RENDERER=x11 (XWayland) to hide the window.", + "skip-taskbar protocol. Pass forceX11 = true for this window, or run with " + + "NUCLEUS_TAO_LINUX_RENDERER=x11 (XWayland) for the whole app.", ) } val window = @@ -311,6 +316,7 @@ internal fun ApplicationScope.openDecoratedWindow( // TaoComposeSceneHost.attach() instead. skipTaskbar = hiddenFromDock, transparent = transparent, + forceX11 = forceX11, // Tao defaults borderless windows to a drop shadow (DWM on // Windows, NSWindow.hasShadow on macOS). Overlays must opt out // or the ghost still shows a soft contour. Fully transparent diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt index 9e520dcd9..e2e1fbe08 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt @@ -155,6 +155,20 @@ public fun ApplicationScope.DecoratedWindow( * caveat. */ visibleOnAllWorkspaces: Boolean = false, + /** + * Linux only: give this window an X11 surface even when the app runs on a + * native Wayland session, by re-homing it on a second `GdkDisplay` opened + * on `DISPLAY` (XWayland). Creation-time only. + * + * Wayland deliberately has no protocol for client-side stacking + * ([alwaysOnTop]), programmatic positioning ([state]`.position`) or + * workspace stickiness ([visibleOnAllWorkspaces]), so an overlay that needs + * them can take an X11 surface for itself while the rest of the app keeps + * its Wayland surfaces — no `NUCLEUS_TAO_LINUX_RENDERER=x11` for the whole + * process. Logs a warning when no X server is reachable and the window + * stays on Wayland. + */ + forceX11: Boolean = false, content: @Composable TaoDecoratedWindowScope.() -> Unit, ) { val latestOnClose by rememberUpdatedState(onCloseRequest) @@ -229,6 +243,7 @@ public fun ApplicationScope.DecoratedWindow( macOSStyle = macOSStyle, hiddenFromDock = hiddenFromDock, initialCompositionLocalContext = compositionLocalContext, + forceX11 = forceX11, content = { val backgroundArgb = latestWindowBackgroundArgb.value val clearColorLayers = LocalWindowClearColorLayers.current diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoApplication.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoApplication.kt index f90cd5984..67a5b0d73 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoApplication.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoApplication.kt @@ -90,6 +90,10 @@ public object TaoApplication { // Drop shadow on borderless windows (Windows DWM / macOS hasShadow). // Set false for overlays (`DecoratedWindow(undecorated)`). undecoratedShadow: Boolean = true, + // Linux only: give this window an X11 surface even on a native Wayland + // session, so it can use the window management Wayland has no protocol + // for. Creation-time only. See [TaoWindow.isNativeWaylandSurface]. + forceX11: Boolean = false, ): TaoWindow { val handle = handleSeq.getAndIncrement() val window = @@ -98,6 +102,7 @@ public object TaoApplication { isResizable = resizable, isPopup = popupOf != null, popupParentHandle = popupOf?.handle ?: 0L, + requestedX11 = forceX11, ) windows[handle] = window val safeWidth = if (width.isFinite() && width > 0.0) width else DEFAULT_WINDOW_WIDTH_DP @@ -115,6 +120,7 @@ public object TaoApplication { skipTaskbar, transparent, undecoratedShadow, + forceX11, ) return window } diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt index cee4167f0..61140bf9b 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoWindow.kt @@ -42,6 +42,12 @@ public class TaoWindow internal constructor( * window as a Wayland `wl_subsurface`. */ internal val popupParentHandle: Long = 0L, + /** + * `true` when the window asked for an X11 surface through + * `openWindow(forceX11 = true)` (Linux only). Kept so [show] can report a + * request the native side could not honour. + */ + internal val requestedX11: Boolean = false, ) { // Snapshot-backed so Compose consumers (WindowControls*, resize hit-test // gating) recompose when resizability changes at runtime — the AWT @@ -711,8 +717,16 @@ public class TaoWindow internal constructor( ) } - /** `true` when this window is backed by a native Wayland surface. */ - private val isNativeWaylandSurface: Boolean + /** + * `true` when this window is backed by a native Wayland surface, `false` + * on X11/XWayland and on every other platform. + * + * Per-window, read from the native surface rather than the environment: + * a window opened with `forceX11` reports `false` inside an app whose other + * windows are Wayland. Only meaningful once the native window exists (after + * `WINDOW_READY`). + */ + public val isNativeWaylandSurface: Boolean get() { if (Platform.Current != Platform.Linux || !NativeTaoBridge.isLoaded) return false val handles = NativeTaoBridge.nativeLinuxHandles(handle) ?: return false @@ -814,6 +828,15 @@ public class TaoWindow internal constructor( // click-through state lands on the mapped window — see // [ignoreCursorEvents]. if (ignoreCursorEvents) NativeTaoBridge.nativeSetIgnoreCursorEvents(handle, true) + // The native surface exists by now, so this is the first point where an + // unhonoured [requestedX11] can be reported. + if (requestedX11 && isNativeWaylandSurface && waylandWarnings.add("forceX11")) { + waylandLogger.warning( + "forceX11 could not give this window an X11 surface — no X server on DISPLAY " + + "(no XWayland in this session?). It stays on Wayland, where stacking, " + + "positioning and workspace stickiness are unavailable.", + ) + } } public fun hide() { diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt index d2e217cb1..86226dc15 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/ffi/NativeTaoBridge.kt @@ -155,6 +155,13 @@ internal object NativeTaoBridge { // false (`DecoratedWindow(undecorated = true)`). Ignored on Linux // (CSD shadow is gated in the host). undecoratedShadow: Boolean, + // Linux: give this window an X11 surface even when the process runs on + // native Wayland, by re-homing it on a second GdkDisplay opened on + // DISPLAY. Creation-time only. Ignored elsewhere and when the process + // is already an X11/XWayland client. Silently keeps the Wayland surface + // when no X server is reachable — callers check the surface kind + // (`nativeLinuxHandles`) rather than a return value. + forceX11: Boolean, ) @JvmStatic diff --git a/decorated-window-tao/src/main/native/src/event_loop.rs b/decorated-window-tao/src/main/native/src/event_loop.rs index 637a8ab65..de16effcc 100644 --- a/decorated-window-tao/src/main/native/src/event_loop.rs +++ b/decorated-window-tao/src/main/native/src/event_loop.rs @@ -85,6 +85,62 @@ fn on_tao_size_move(window_id: tao::window::WindowId, active: bool) { dispatch(handle, EVENT_SIZE_MOVE, if active { 1 } else { 0 }, 0); } +/// Moves a freshly built window onto an X11 screen while the rest of the +/// process keeps talking native Wayland. +/// +/// GTK supports several `GdkDisplay`s in one process and one main loop, so we +/// open the X server named by `DISPLAY` (XWayland on a Wayland session) once +/// and re-home the window's `GdkWindow` there. That buys back everything +/// xdg-shell has no protocol for — stacking (`alwaysOnTop`), programmatic +/// positioning and workspace stickiness — for overlays that need it, without +/// forcing the whole app onto XWayland. +/// +/// `gtk_window_set_screen` unrealizes the widget, so we realize it again to +/// restore the invariant tao patch 0003 establishes: the `GdkWindow` is valid +/// when window creation returns, before `WINDOW_READY` reaches the JVM and the +/// renderer attaches to it. +#[cfg(target_os = "linux")] +fn move_window_to_x11(window: &tao::window::Window) { + use gtk::prelude::*; + use tao::platform::unix::WindowExtUnix; + + let gtk_window = window.gtk_window(); + if !gtk_window.display().backend().is_wayland() { + return; // already an X11 / XWayland client — nothing to do. + } + // No X server to fall back on (DISPLAY unset, no XWayland): keep the + // Wayland surface. The Kotlin side notices — the window still reports a + // Wayland surface kind — and logs it there, where the framework's JUL + // facade lives. + let Some(x11) = x11_display() else { + return; + }; + gtk_window.set_screen(&x11.default_screen()); + gtk_window.realize(); +} + +/// The X11 `GdkDisplay`, opened on first use and kept for the process. Lives in +/// a thread-local because `GdkDisplay` is neither `Send` nor `Sync` and every +/// caller runs on the event-loop thread. +#[cfg(target_os = "linux")] +fn x11_display() -> Option { + use std::cell::RefCell; + thread_local! { + static X11_DISPLAY: RefCell>> = const { RefCell::new(None) }; + } + X11_DISPLAY.with(|cell| { + cell.borrow_mut() + .get_or_insert_with(|| { + let name = std::env::var("DISPLAY").ok()?; + // GDK tries its backends in order for the given name; the + // Wayland backend cannot parse an X11 display name, so this + // resolves to the X11 backend even on a Wayland session. + gtk::gdk::Display::open(&name) + }) + .clone() + }) +} + pub(crate) fn run_event_loop_blocking() { // GTK backend selection. Default: let GDK auto-pick (= native Wayland on // a Wayland session, X11 elsewhere). The Wayland-native path goes through @@ -196,6 +252,7 @@ pub(crate) fn run_event_loop_blocking() { skip_taskbar, transparent, undecorated_shadow, + force_x11, } => { #[allow(unused_mut)] let mut builder = WindowBuilder::new() @@ -276,6 +333,12 @@ pub(crate) fn run_event_loop_blocking() { } let window = builder.build(target); if let Ok(window) = window { + #[cfg(target_os = "linux")] + if force_x11 { + move_window_to_x11(&window); + } + #[cfg(not(target_os = "linux"))] + let _ = force_x11; let logical_w = width as jint; let logical_h = height as jint; diff --git a/decorated-window-tao/src/main/native/src/events.rs b/decorated-window-tao/src/main/native/src/events.rs index e8c69f2f8..1a10dfb98 100644 --- a/decorated-window-tao/src/main/native/src/events.rs +++ b/decorated-window-tao/src/main/native/src/events.rs @@ -277,6 +277,12 @@ pub(crate) enum UserEvent { // CSD (`with_csd_hidden_titlebar`, Wayland only). False for borderless // overlays. undecorated_shadow: bool, + // Linux: put this window on an X11 screen even when the process runs + // on native Wayland, so it can use the window-management features + // Wayland has no protocol for (stacking, programmatic positioning, + // workspace stickiness). Ignored elsewhere, and already satisfied when + // the process is an X11/XWayland client. + force_x11: bool, }, SetVisible { handle: u64, diff --git a/decorated-window-tao/src/main/native/src/window_jni.rs b/decorated-window-tao/src/main/native/src/window_jni.rs index 9aa15f8f6..ef1fa4c9c 100644 --- a/decorated-window-tao/src/main/native/src/window_jni.rs +++ b/decorated-window-tao/src/main/native/src/window_jni.rs @@ -71,6 +71,7 @@ pub extern "system" fn Java_dev_nucleusframework_window_tao_ffi_NativeTaoBridge_ skip_taskbar: jboolean, transparent: jboolean, undecorated_shadow: jboolean, + force_x11: jboolean, ) { let title: String = match env.get_string(&title) { Ok(s) => s.into(), @@ -89,6 +90,7 @@ pub extern "system" fn Java_dev_nucleusframework_window_tao_ffi_NativeTaoBridge_ skip_taskbar: skip_taskbar != JNI_FALSE, transparent: transparent != JNI_FALSE, undecorated_shadow: undecorated_shadow != JNI_FALSE, + force_x11: force_x11 != JNI_FALSE, }); } diff --git a/decorated-window-tao/src/main/native/vendor/tao-patches/0006-linux-cursor-ignore-events-region.patch b/decorated-window-tao/src/main/native/vendor/tao-patches/0006-linux-cursor-ignore-events-region.patch index d9f0c7df9..8c25c1161 100644 --- a/decorated-window-tao/src/main/native/vendor/tao-patches/0006-linux-cursor-ignore-events-region.patch +++ b/decorated-window-tao/src/main/native/vendor/tao-patches/0006-linux-cursor-ignore-events-region.patch @@ -1,42 +1,11 @@ --- a/src/platform_impl/linux/event_loop.rs +++ b/src/platform_impl/linux/event_loop.rs -@@ -495,15 +495,30 @@ impl EventLoop { - } - } - WindowRequest::CursorIgnoreEvents(ignore) => { -- if ignore { -- let empty_region = Region::create_rectangle(&RectangleInt::new(0, 0, 1, 1)); -- window -- .window() -- .unwrap() -- .input_shape_combine_region(&empty_region, 0, 0); -- } else { -- window.input_shape_combine_region(None) -- }; -+ // PATCH(nucleus): an *empty* region, not a 1x1 rectangle at the -+ // origin — upstream leaves the top-left pixel clickable. Both -+ // branches also go through the same GdkWindow: upstream cleared -+ // the shape on the GtkWidget, which is a no-op when the shape was -+ // installed on the GdkWindow, so click-through could never be -+ // turned back off. -+ if let Some(gdk_window) = window.window() { -+ if ignore { -+ gdk_window.input_shape_combine_region(&Region::create(), 0, 0); -+ } else { -+ // Only a NULL region clears the shape for good; a full-window -+ // region would go stale on the next resize, and the safe -+ // binding cannot express NULL. -+ use glib::translate::ToGlibPtr; -+ unsafe { -+ gdk::ffi::gdk_window_input_shape_combine_region( -+ gdk_window.to_glib_none().0, -+ std::ptr::null_mut(), -+ 0, -+ 0, -+ ); -+ } -+ } -+ } - } - WindowRequest::ProgressBarState(_) => unreachable!(), - WindowRequest::BadgeCount(_, _) => unreachable!(), +@@ -12,7 +12,7 @@ use std::{ + time::Instant, + }; + +-use cairo::{RectangleInt, Region}; ++use cairo::Region; + use crossbeam_channel::SendError; + use gdk::{Cursor, CursorType, EventKey, EventMask, ScrollDirection, WindowEdge, WindowState}; + use gio::Cancellable; diff --git a/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/linux/event_loop.rs b/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/linux/event_loop.rs index 7d9501503..f8eeba4c4 100644 --- a/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/linux/event_loop.rs +++ b/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/linux/event_loop.rs @@ -12,7 +12,7 @@ use std::{ time::Instant, }; -use cairo::{RectangleInt, Region}; +use cairo::Region; use crossbeam_channel::SendError; use gdk::{Cursor, CursorType, EventKey, EventMask, ScrollDirection, WindowEdge, WindowState}; use gio::Cancellable; diff --git a/examples/rect-stress-demo/api/rect-stress-demo.api b/examples/rect-stress-demo/api/rect-stress-demo.api new file mode 100644 index 000000000..141cda962 --- /dev/null +++ b/examples/rect-stress-demo/api/rect-stress-demo.api @@ -0,0 +1,12 @@ +public final class com/example/rectstress/ComposableSingletons$MainKt { + public static final field INSTANCE Lcom/example/rectstress/ComposableSingletons$MainKt; + public fun ()V + public final fun getLambda$-1782098224$Nucleus_examples_rect_stress_demo ()Lkotlin/jvm/functions/Function3; + public final fun getLambda$-535725003$Nucleus_examples_rect_stress_demo ()Lkotlin/jvm/functions/Function4; + public final fun getLambda$1066003079$Nucleus_examples_rect_stress_demo ()Lkotlin/jvm/functions/Function3; +} + +public final class com/example/rectstress/MainKt { + public static final fun main ([Ljava/lang/String;)V +} + diff --git a/nucleus-application/api/nucleus-application.api b/nucleus-application/api/nucleus-application.api index e0ba36642..73ac6c242 100644 --- a/nucleus-application/api/nucleus-application.api +++ b/nucleus-application/api/nucleus-application.api @@ -9,8 +9,8 @@ public final class dev/nucleusframework/application/DecoratedDialogKt { } public final class dev/nucleusframework/application/DecoratedWindowKt { - public static final fun DecoratedWindow-Uc0goEM (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V - public static final fun DecoratedWindow-XZmG138 (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun DecoratedWindow-7qYZe8E (Ldev/nucleusframework/application/NucleusApplicationScope;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun DecoratedWindow-MOr4QTY (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZZLdev/nucleusframework/application/NucleusWindow;ZZZLandroidx/compose/ui/unit/DpSize;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class dev/nucleusframework/application/DefaultNucleusDialogHost : dev/nucleusframework/application/NucleusDialogHost { diff --git a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt index 2c731648a..587ced844 100644 --- a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt +++ b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt @@ -79,6 +79,14 @@ public fun NucleusApplicationScope.DecoratedWindow( // [hiddenFromDock] window already shows on every virtual desktop. Reactive. // Honoured by the Tao backend; the AWT backend ignores it. visibleOnAllWorkspaces: Boolean = false, + // Linux only: give this window an X11 surface even when the app runs on a + // native Wayland session (a second GdkDisplay opened on DISPLAY, i.e. + // XWayland). Creation-time only. Wayland has no protocol for client-side + // stacking, programmatic positioning or workspace stickiness, so an overlay + // that needs them can take an X11 surface for itself while the rest of the + // app keeps its Wayland surfaces. Honoured by the Tao backend; ignored by + // the AWT backend and on other platforms. + forceX11: Boolean = false, content: @Composable NucleusDecoratedWindowScope.() -> Unit, ) { when (this) { @@ -131,6 +139,7 @@ public fun NucleusApplicationScope.DecoratedWindow( transparent = transparent, clickThrough = clickThrough, visibleOnAllWorkspaces = visibleOnAllWorkspaces, + forceX11 = forceX11, popupFor = popupFor, nativePopupLayers = nativePopupLayers, nativeContextMenu = nativeContextMenu, @@ -176,6 +185,7 @@ public fun DecoratedWindow( transparent: Boolean = false, clickThrough: Boolean = false, visibleOnAllWorkspaces: Boolean = false, + forceX11: Boolean = false, content: @Composable NucleusDecoratedWindowScope.() -> Unit, ) { LocalNucleusApplicationScope.current.DecoratedWindow( @@ -199,6 +209,7 @@ public fun DecoratedWindow( transparent = transparent, clickThrough = clickThrough, visibleOnAllWorkspaces = visibleOnAllWorkspaces, + forceX11 = forceX11, content = content, ) } diff --git a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt index 71bab4c3f..1860adf6c 100644 --- a/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt +++ b/nucleus-application/src/main/kotlin/dev/nucleusframework/application/internal/TaoDecoratedWindowAdapter.kt @@ -51,6 +51,7 @@ internal object TaoDecoratedWindowAdapter { transparent: Boolean, clickThrough: Boolean, visibleOnAllWorkspaces: Boolean, + forceX11: Boolean, popupFor: NucleusWindow?, nativePopupLayers: Boolean, nativeContextMenu: Boolean, @@ -94,6 +95,7 @@ internal object TaoDecoratedWindowAdapter { transparent = transparent, clickThrough = clickThrough, visibleOnAllWorkspaces = visibleOnAllWorkspaces, + forceX11 = forceX11, popupFor = popupFor?.unsafe?.taoWindow, nativePopupLayers = nativePopupLayers, hiddenFromDock = hiddenFromDock, From e5aaf4837399a5c7d7b3472ab04903fd41ea0590 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Mon, 17 Aug 2026 19:18:58 +0300 Subject: [PATCH 10/12] fix(tao): retry an Aligned window position until the native window exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolving WindowPosition.Aligned needs the monitor work area, which Linux queries through the native window — and Tao creates that asynchronously on its event loop. A JVM start is slow enough that the window is already there at first composition; a GraalVM native-image start beats it, the query returns nothing, and since the effect only re-runs when state.position changes the window stayed wherever the WM had centred it until the app moved it by hand. Retries over ~10 frames, then gives up rather than looping forever so a genuinely unavailable monitor query cannot wedge the effect. --- .../window/tao/DecoratedWindowComposable.kt | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt index e2e1fbe08..ececc6e82 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt @@ -30,6 +30,7 @@ import dev.nucleusframework.window.tao.ffi.NativeTaoBridge import dev.nucleusframework.window.tao.ffi.NativeTaoMacOsDecoBridge import dev.nucleusframework.window.tao.ffi.NativeTaoWindowsDecoBridge import dev.nucleusframework.window.tao.ffi.toRgbaIcon +import kotlinx.coroutines.delay import kotlin.math.roundToInt /** @@ -418,7 +419,21 @@ public fun ApplicationScope.DecoratedWindow( // `state.size` still holds the (smaller) requested size at this // point. val effectiveSize = effectiveAlignedSize(state.size, minimumSize) - if (applyAlignedPosition(window, pos, effectiveSize)) { + // Resolving an alignment needs the monitor work area, which + // Linux queries through the native window — and Tao creates + // that asynchronously on its event loop. A JVM start is slow + // enough that it is already there at first composition; a + // native-image start is not, and a single failed attempt left + // the window wherever the WM had centred it, for good, since + // this effect only re-runs when `state.position` changes. + var landed = applyAlignedPosition(window, pos, effectiveSize) + var attempt = 0 + while (!landed && attempt < ALIGNED_POSITION_RETRIES) { + delay(ALIGNED_POSITION_RETRY_MS) + attempt++ + landed = applyAlignedPosition(window, pos, effectiveSize) + } + if (landed) { applied.position = pos } } @@ -699,6 +714,15 @@ private fun parentOuterOriginLogical(parent: TaoWindow): Pair? { * (slot 0: 1 = Xlib, 2 = Wayland) so we don't have to second-guess GDK env * vars or the auto-pick logic in `event_loop.rs`. */ +/** + * How long [applyAlignedPosition] keeps retrying while the native window is + * still being created on the Tao event loop — ~10 frames at 60 Hz, far past + * any observed startup, and given up on rather than looped forever so a + * genuinely unavailable monitor query cannot wedge the effect. + */ +private const val ALIGNED_POSITION_RETRIES = 10 +private const val ALIGNED_POSITION_RETRY_MS = 16L + private val waylandPositionWarned = java.util.concurrent.atomic .AtomicBoolean(false) From 8fe7e4fdcecde727d5f0ab366055e5f54982bf29 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Mon, 17 Aug 2026 19:18:58 +0300 Subject: [PATCH 11/12] feat(examples): drive the watermark from a second, ordinary window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The demo was a single overlay with a tray menu. It now opens a Material 3 control window — toggle the overlay, pick the corner from a miniature screen, and read which surface each window actually landed on. On Linux that makes the demo the mixed case it is meant to show: the control window keeps the session's Wayland surface while the overlay takes an X11 one through forceX11, instead of pushing the whole app onto XWayland. --- examples/watermark-demo/build.gradle.kts | 2 + .../main/kotlin/com/example/watermark/Main.kt | 263 ++++++++++++++++-- 2 files changed, 242 insertions(+), 23 deletions(-) diff --git a/examples/watermark-demo/build.gradle.kts b/examples/watermark-demo/build.gradle.kts index b0fb1a67d..eff69a4a3 100644 --- a/examples/watermark-demo/build.gradle.kts +++ b/examples/watermark-demo/build.gradle.kts @@ -11,7 +11,9 @@ dependencies { implementation(project(":decorated-window-tao")) implementation(project(":nucleus-application")) implementation(project(":core-runtime")) + implementation(project(":decorated-window-material3")) implementation(compose.desktop.currentOs) + implementation(compose.material3) implementation(libs.composenativetray) } diff --git a/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt index 9077c200d..4a8130c37 100644 --- a/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt +++ b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt @@ -8,12 +8,27 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.BasicText +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -34,6 +49,10 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.rememberWindowState import dev.nucleusframework.application.DecoratedWindow import dev.nucleusframework.application.nucleusApplication +import dev.nucleusframework.core.runtime.Platform +import dev.nucleusframework.window.TitleBar +import dev.nucleusframework.window.material.MaterialDecoratedWindow +import dev.nucleusframework.window.tao.TaoWindow import dev.nucleusframework.composenativetray.tray.api.Tray import java.io.File @@ -55,12 +74,16 @@ import java.io.File * which the Tao backend resolves against the primary monitor's work area at * runtime. * + * A second, ordinary window drives it: toggle the watermark on and off, pick + * its corner. That window keeps the session's native surface, which on Linux + * makes the demo the mixed case — a Wayland app with one X11 window. + * * **Linux**: an overlay like this needs X11 semantics — stacking * (`alwaysOnTop`), programmatic positioning (`WindowPosition.Aligned`) and - * workspace stickiness are all absent from the Wayland protocol, and the Tao - * backend logs a warning for each. Run under XWayland - * (`NUCLEUS_TAO_LINUX_RENDERER=x11`) or give the overlay window its own X11 - * surface while the rest of the app stays on Wayland. + * workspace stickiness are all absent from the Wayland protocol. Rather than + * pushing the whole app onto XWayland with + * `NUCLEUS_TAO_LINUX_RENDERER=x11`, the watermark asks for an X11 surface of + * its own (`forceX11`); the control window stays on Wayland. * * The tray icon is fed as image *files* (the `iconPath` overload): the * composable-icon overloads render through a skiko `Image.encodeToData` @@ -77,6 +100,7 @@ fun main(args: Array) = size = DpSize(360.dp, 160.dp), ) var corner by remember { mutableStateOf(Alignment.BottomEnd) } + var watermarkVisible by remember { mutableStateOf(true) } fun moveTo(alignment: Alignment) { corner = alignment @@ -88,6 +112,12 @@ fun main(args: Array) = windowsIconPath = trayIconIco, tooltip = "Nucleus watermark", ) { + CheckableItem( + label = "Show watermark", + checked = watermarkVisible, + onCheckedChange = { watermarkVisible = it }, + ) + Divider() CheckableItem( label = "Top left", checked = corner == Alignment.TopStart, @@ -112,25 +142,53 @@ fun main(args: Array) = Item(label = "Quit") { exitApplication() } } - DecoratedWindow( - onCloseRequest = ::exitApplication, - state = state, - title = "Watermark", - resizable = false, - alwaysOnTop = true, - undecorated = true, - transparent = true, - hiddenFromDock = true, - focusable = false, - // A watermark must never get in the way: clicks fall through to - // whatever is underneath and the window never takes focus. - clickThrough = true, - // …and it follows the user across desktops. macOS/Linux need this - // explicitly; on Windows a taskbar-excluded window already shows on - // every virtual desktop. - visibleOnAllWorkspaces = true, - ) { - AnimatedWatermark() + var overlaySurface by remember { mutableStateOf(null) } + + MaterialTheme(colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()) { + MaterialDecoratedWindow( + onCloseRequest = ::exitApplication, + state = rememberWindowState(size = DpSize(440.dp, 470.dp)), + title = "Watermark control", + ) { + TitleBar { + Text("Watermark control", fontSize = 13.sp, fontWeight = FontWeight.Medium) + } + ControlPanel( + watermarkVisible = watermarkVisible, + onWatermarkVisibleChange = { watermarkVisible = it }, + corner = corner, + onCornerChange = ::moveTo, + controlSurface = surfaceLabel(nucleusWindow.unsafe.taoWindow), + overlaySurface = overlaySurface, + ) + } + } + + if (watermarkVisible) { + DecoratedWindow( + onCloseRequest = { watermarkVisible = false }, + state = state, + title = "Watermark", + resizable = false, + alwaysOnTop = true, + undecorated = true, + transparent = true, + hiddenFromDock = true, + focusable = false, + // A watermark must never get in the way: clicks fall through to + // whatever is underneath and the window never takes focus. + clickThrough = true, + // …and it follows the user across desktops. macOS/Linux need this + // explicitly; on Windows a taskbar-excluded window already shows on + // every virtual desktop. + visibleOnAllWorkspaces = true, + // Linux: everything above needs an X11 surface. Only this window + // gets one — the control window stays on the session's compositor. + forceX11 = true, + ) { + LaunchedEffect(nucleusWindow) { overlaySurface = surfaceLabel(nucleusWindow.unsafe.taoWindow) } + AnimatedWatermark() + } } } @@ -209,3 +267,162 @@ private fun AnimatedWatermark() { ) } } + +/** + * Which windowing system actually backs a window, read from the live surface + * rather than from the environment — the whole point of `forceX11` is that the + * answer differs between two windows of the same process. `null` on + * macOS/Windows, where the question does not arise. + */ +private fun surfaceLabel(window: TaoWindow?): String? { + if (window == null || Platform.Current != Platform.Linux) return null + return if (window.isNativeWaylandSurface) "Wayland" else "X11" +} + +/** + * The control window's content: toggle the overlay, pick the corner it is + * pinned to, and see which surface each of the two windows ended up on. + */ +@Composable +private fun ControlPanel( + watermarkVisible: Boolean, + onWatermarkVisibleChange: (Boolean) -> Unit, + corner: Alignment, + onCornerChange: (Alignment) -> Unit, + controlSurface: String?, + overlaySurface: String?, +) { + Column( + modifier = + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 24.dp, vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Text("Watermark overlay", style = MaterialTheme.typography.titleMedium) + Text( + text = "Transparent, click-through, always on top", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Switch(checked = watermarkVisible, onCheckedChange = onWatermarkVisibleChange) + } + + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = "Pinned corner", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + CornerPicker(corner = corner, enabled = watermarkVisible, onCornerChange = onCornerChange) + } + + if (controlSurface != null) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = "Surfaces", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + SurfaceRow("This window", controlSurface) + SurfaceRow("Overlay", overlaySurface ?: "—") + } + } + } +} + +/** A miniature screen: click a corner to pin the overlay there. */ +@Composable +private fun CornerPicker( + corner: Alignment, + enabled: Boolean, + onCornerChange: (Alignment) -> Unit, +) { + val corners = + listOf( + Alignment.TopStart to Alignment.TopStart, + Alignment.TopEnd to Alignment.TopEnd, + Alignment.BottomStart to Alignment.BottomStart, + Alignment.BottomEnd to Alignment.BottomEnd, + ) + Box( + modifier = + Modifier + .fillMaxWidth() + .height(150.dp) + .clip(RoundedCornerShape(14.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + shape = RoundedCornerShape(14.dp), + ).padding(10.dp), + ) { + corners.forEach { (alignment, value) -> + val selected = corner == value + Box( + modifier = + Modifier + .align(alignment) + .size(width = 116.dp, height = 52.dp) + .clip(RoundedCornerShape(10.dp)) + .background( + if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surface + }, + ).clickable(enabled = enabled) { onCornerChange(value) }, + contentAlignment = Alignment.Center, + ) { + Text( + text = "NUCLEUS", + style = MaterialTheme.typography.labelSmall, + letterSpacing = 2.sp, + color = + if (selected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + }, + ) + } + } + } +} + +/** `label — backend` line, with the backend rendered as a pill. */ +@Composable +private fun SurfaceRow( + label: String, + backend: String, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(label, style = MaterialTheme.typography.bodyMedium) + Box( + modifier = + Modifier + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.secondaryContainer) + .padding(horizontal = 12.dp, vertical = 4.dp), + ) { + Text( + text = backend, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } + } +} From 842efd730440d4d4921946dca065b8071d3580b9 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Mon, 17 Aug 2026 19:44:36 +0300 Subject: [PATCH 12/12] style(tao): satisfy detekt and ktlint on the new window flags Three lint regressions from the previous commits, all mechanical: the hiddenFromDock guard grew a fifth condition (fold forceX11 into the X11 predicate it belongs to, which also reads better), the Aligned retry constants landed between an existing KDoc and its declaration, and the demo's imports were out of order. --- .../window/tao/DecoratedWindow.kt | 13 +++++++------ .../window/tao/DecoratedWindowComposable.kt | 18 +++++++++--------- .../main/kotlin/com/example/watermark/Main.kt | 8 ++++---- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt index 66f04a10e..9f05639a3 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindow.kt @@ -270,16 +270,17 @@ internal fun ApplicationScope.openDecoratedWindow( // native Wayland does not honour: there is no client-side skip-taskbar // protocol on Wayland (xdg-shell, gtk_shell1 and the staging extensions all // lack it, and Mutter rejects wlr-layer-shell). It is effective only under - // X11/XWayland. Warn so the no-op isn't silent — force XWayland with - // NUCLEUS_TAO_LINUX_RENDERER=x11 to actually hide the window. - val forcesXWayland = - System.getenv("GDK_BACKEND").orEmpty().equals("x11", ignoreCase = true) || + // X11/XWayland. Warn so the no-op isn't silent — either take an X11 + // surface for this window ([forceX11]) or put the whole app on XWayland + // with NUCLEUS_TAO_LINUX_RENDERER=x11. + val willBeX11 = + forceX11 || + System.getenv("GDK_BACKEND").orEmpty().equals("x11", ignoreCase = true) || System.getenv("NUCLEUS_TAO_LINUX_RENDERER").orEmpty().equals("x11", ignoreCase = true) if (hiddenFromDock && Platform.Current == Platform.Linux && Platform.isWayland && - !forcesXWayland && - !forceX11 + !willBeX11 ) { hiddenFromDockLogger.warning( "hiddenFromDock has no effect on native Wayland: Wayland has no client-side " + diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt index ececc6e82..00dfa0c96 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/DecoratedWindowComposable.kt @@ -548,6 +548,15 @@ private fun WindowState.applyMacOsInitialMaximizedSize() { } } +/** + * How long [applyAlignedPosition] keeps retrying while the native window is + * still being created on the Tao event loop — ~10 frames at 60 Hz, far past + * any observed startup, and given up on rather than looped forever so a + * genuinely unavailable monitor query cannot wedge the effect. + */ +private const val ALIGNED_POSITION_RETRIES = 10 +private const val ALIGNED_POSITION_RETRY_MS = 16L + /** * Resolves a [WindowPosition.Aligned] against the primary monitor's work area * and pushes the resulting outer position to [window]. Returns `true` when the @@ -714,15 +723,6 @@ private fun parentOuterOriginLogical(parent: TaoWindow): Pair? { * (slot 0: 1 = Xlib, 2 = Wayland) so we don't have to second-guess GDK env * vars or the auto-pick logic in `event_loop.rs`. */ -/** - * How long [applyAlignedPosition] keeps retrying while the native window is - * still being created on the Tao event loop — ~10 frames at 60 Hz, far past - * any observed startup, and given up on rather than looped forever so a - * genuinely unavailable monitor query cannot wedge the effect. - */ -private const val ALIGNED_POSITION_RETRIES = 10 -private const val ALIGNED_POSITION_RETRY_MS = 16L - private val waylandPositionWarned = java.util.concurrent.atomic .AtomicBoolean(false) diff --git a/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt index 4a8130c37..908b75d4a 100644 --- a/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt +++ b/examples/watermark-demo/src/main/kotlin/com/example/watermark/Main.kt @@ -17,15 +17,15 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -49,11 +49,11 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.rememberWindowState import dev.nucleusframework.application.DecoratedWindow import dev.nucleusframework.application.nucleusApplication +import dev.nucleusframework.composenativetray.tray.api.Tray import dev.nucleusframework.core.runtime.Platform import dev.nucleusframework.window.TitleBar import dev.nucleusframework.window.material.MaterialDecoratedWindow import dev.nucleusframework.window.tao.TaoWindow -import dev.nucleusframework.composenativetray.tray.api.Tray import java.io.File /**