Skip to content

feat: expose transparent + clickThrough windows, watermark demo - #563

Merged
kdroidFilter merged 12 commits into
nucleus-2.5from
feat/expose-transparent-nucleus-application
Aug 17, 2026
Merged

feat: expose transparent + clickThrough windows, watermark demo#563
kdroidFilter merged 12 commits into
nucleus-2.5from
feat/expose-transparent-nucleus-application

Conversation

@kdroidFilter

@kdroidFilter kdroidFilter commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • transparent on the backend-agnostic DecoratedWindow (nucleus-application): forwards the Tao backend's per-pixel transparency flag (Fully transparent Windows #416) through both overloads and TaoDecoratedWindowAdapter; ignored by the AWT backend, like undecorated. API dumps regenerated.
  • No window contour on fully transparent windows (Windows): the custom chrome still applied the themed DWM 1px frame (DWMWA_BORDER_COLOR), the drop-shadow margin, the rounded clip and tao's style-level undecorated shadow — all tracing the rectangular HWND. Transparent windows now reuse the borderlessChrome DWM treatment (hit-testing untouched) and drop with_undecorated_shadow on Windows; the Linux/Windows Compose CSD stroke is skipped too. macOS keeps its content-shaped AppKit shadow.
  • clickThrough windows: plumbs tao's set_ignore_cursor_events through the JNI bridge (TaoWindow.setIgnoreCursorEvents) and exposes a reactive clickThrough parameter on the Tao and backend-agnostic DecoratedWindow composables. On Windows tao only flips WS_EX_TRANSPARENT|WS_EX_LAYERED; a layered window renders nothing until its layering attributes are initialised, so the native handler calls SetLayeredWindowAttributes(alpha=255).
  • examples/watermark-demo: animated NUCLEUS watermark in a transparent, undecorated, always-on-top, click-through, never-focused, taskbar-hidden window pinned to a screen corner, driven entirely from a ComposeNativeTray icon (corner selection via WindowPosition.Aligned + quit). Documents two ComposeNativeTray 2.1.0 workarounds: composable-icon overloads crash on Compose 1.12's skiko (NoSuchMethodError on Image.encodeToData), and iconPath must be a file: URI because java.net.URI rejects raw Windows paths.

Test plan

  • :decorated-window-tao / :nucleus-application: compile, tests, ktlint, detekt, apiCheck
  • Windows manual: transparent window shows no DWM frame, no shadow, no CSD stroke; watermark demo renders with clickThrough = true and clicks fall through; window never takes focus
  • macOS / Linux manual pass on transparent + clickThrough
  • ComposeNativeTray: file an upstream issue for the skiko encodeToData crash on Compose 1.12

…ndow

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.
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.
…ents

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.
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.
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.
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).
`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.
…no-op

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.
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.
…ists

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.
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.
@kdroidFilter
kdroidFilter marked this pull request as ready for review August 17, 2026 16:24
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.
@kdroidFilter
kdroidFilter merged commit 922f46b into nucleus-2.5 Aug 17, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant