Skip to content

Commit 59a15d1

Browse files
committed
feat: route Linux TrayApp to the standalone transparent panel
Nucleus 2.0.0-alpha-202607120617 ships the Linux TaoStandalonePopup (raw X11 override-redirect ARGB window, working through XWayland on Wayland sessions). The router now sends Linux to TrayAppImplPanel, gated on isTaoStandalonePopupAvailable() so rare Wayland-only setups without an X server keep the opaque-window fallback.
1 parent 6475e56 commit 59a15d1

3 files changed

Lines changed: 83 additions & 21 deletions

File tree

docs/PLAN_MACOS_LINUX_PANEL.md

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,24 +92,76 @@ really contains the fresh dylib, md5 vs `src/main/resources/nucleus/native/…`)
9292
→ recompile ComposeNativeTray → hand off. Never compile while a demo JVM
9393
is running.
9494

95-
## Linux (decide, then maybe implement)
96-
97-
Transparency requires a compositing WM + ARGB visual; there is no DComp/
98-
CAMetalLayer equivalent that works everywhere (X11 vs Wayland, GNOME vs
99-
KDE vs bare WMs). Options:
100-
101-
- **A (pragmatic, recommended default)**: keep the opaque
102-
`TrayAppImplWindow`. Accept animations over the window background, or
103-
set Linux defaults back to `None` (one-line change in the
104-
`defaultTrayApp*Transition` vals).
105-
- **B (best-effort)**: GTK layer — an undecorated, override-redirect
106-
GTK window with RGBA visual when a compositor is present
107-
(`gdk_screen_is_composited`), falling back to A otherwise. Sizeable
108-
native work; only worth it if users ask.
109-
110-
Outside-click on Wayland cannot use global pointer grabs from a regular
111-
client — focus-loss dismissal (current behavior) is the only reliable
112-
signal. Keep it.
95+
## Linux (option B chosen — raw X11/XWayland panel, IMPLEMENTED)
96+
97+
Status 2026-07-12: macOS is DONE (Nucleus PR #302, `TaoStandalonePopupHostMac`
98+
+ `popup_panel.m`; TrayApp already routes macOS to `TrayAppImplPanel`).
99+
Nucleus repo moved to `~/IdeaProjects/Nucleus`. Linux option B implemented on
100+
Nucleus branch `feat/standalone-popup-panel-linux` (commit 1f037863): native
101+
module + host + routing + smoke test (passes on GNOME Wayland via XWayland)
102+
+ GraalVM metadata + CI. Published locally as `2.0.0-tao-local`;
103+
ComposeNativeTray routes Linux → `TrayAppImplPanel` gated on
104+
`isTaoStandalonePopupAvailable()`. AWAITING: user demo validation on
105+
GNOME Wayland (topmost stacking above Wayland windows, position vs tray
106+
click coords, keyboard into text fields, outside-click dismissal), then
107+
Nucleus PR + published alpha + drop the mavenLocal pin.
108+
109+
### Decision: raw X11 window, NOT a GTK window
110+
111+
GDK's backend is process-wide (native Wayland on Wayland sessions), so a
112+
GTK popup window would only work by forcing the whole app onto XWayland
113+
(`NUCLEUS_TAO_LINUX_RENDERER=x11`). Instead the panel is a **raw X11
114+
override-redirect ARGB32 window on its own `XOpenDisplay` connection**
115+
an independent X client that works even while the app itself is a native
116+
Wayland client, via XWayland (present on effectively all desktops).
117+
Feasibility was validated on GNOME Wayland (Ubuntu, Mutter): OR window
118+
maps and moves at exact global coords, ARGB visual + desktop-GL 3.3 EGL
119+
context all work through XWayland.
120+
121+
Native Wayland (no XWayland/DISPLAY) stays unsupported: no layer-shell in
122+
vendored Tao, `gtk_window_move` is a no-op on xdg-toplevels, `keep_above`
123+
ignored. In that case the panel reports unavailable and TrayApp falls back
124+
to the opaque `TrayAppImplWindow`.
125+
126+
### Architecture (mirrors Windows, reuses the Linux EGL bridge)
127+
128+
- **Visual selection**: query EGL first (alpha=8 desktop-GL configs →
129+
`EGL_NATIVE_VISUAL_ID`), create the window with exactly that visual —
130+
guarantees `NativeTaoEglBridge.nativeAttachX11` matches directly (no
131+
child-window fallback, alpha preserved).
132+
- **Rendering**: reuse `nucleus_tao_egl.c` per-attachment API
133+
(`nativeAttachX11`/`nativeMakeCurrent`/`nativePresent`/`nativeResize` +
134+
`nativeGetProcAddrFunctionPointer`) — Linux convention is one EGL context
135+
per surface, no headless bootstrap needed. `eglSwapInterval(0)` +
136+
the 60 fps pacer pattern from the Windows host (don't block the Tao
137+
main thread on vsync).
138+
- **Threading**: two X connections. Command connection owned by the Tao
139+
main thread (create/move/map/cursor/focus + EGL). Event connection owned
140+
by a dedicated per-panel thread (`XSelectInput` on the panel XID works
141+
cross-connection); quit via ClientMessage. No `XInitThreads` dependency.
142+
- **Input**: Button/Motion/scroll(buttons 4-7) → `TaoNativeWireFormat`
143+
pointer wire; keys send the raw **X keysym** as vkCode + Unicode
144+
codePoint; a new `linuxNativeKeyToAwt` in `dispatchNativeKeyEvent`
145+
translates keysym → AWT VK (same pattern as `macNativeKeyToAwt`).
146+
- **Keyboard focus**: `XSetInputFocus(RevertToParent)` on click while
147+
focusable (Windows `takeKeyboardFocus` equivalent). If some WMs don't
148+
deliver keys to OR windows, escalate to `XGrabKeyboard` later.
149+
- **Outside-click**: XI2 raw ButtonPress on the root window (the X11
150+
analog of `WH_MOUSE_LL`; multiple clients allowed, no grab, doesn't
151+
consume). Fully global on X11 sessions; under XWayland only fires while
152+
X11 surfaces have input — acceptable because the tray-icon toggle and
153+
focus-loss cover the rest.
154+
- **Scale**: `Xft.dpi`-derived (X clients live in the X coordinate space,
155+
which under XWayland is logical — GDK's Wayland scale would mis-size the
156+
panel). Fractional/HiDPI XWayland caveats accepted for v1.
157+
158+
New files: `nucleus_tao_linux_popup.c` (→ `libnucleus_tao_linux_popup.so`,
159+
dlopen libX11/libXi only, linked `-ldl` like siblings),
160+
`PopupNativeBridgeLinux.kt`, `render/TaoStandalonePopupHostLinux.kt`,
161+
`render/TaoKeyLinux.kt`, smoke test guarded on Linux + `DISPLAY`. Plus:
162+
route Linux in `TaoStandalonePopup.kt`, a public availability check for
163+
TrayApp's router, `linux/build.sh` step, GraalVM reachability metadata,
164+
CI `build-natives.yaml` + verify arrays in consumer workflows.
113165

114166
## Also pending (unrelated to the port)
115167

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ kotlinx-coroutines = "1.11.0"
55
compose = "1.11.1"
66
detekt = "1.23.8"
77
ktlint = "12.1.2"
8-
nucleus = "2.0.0-alpha-202607120312"
9-
nucleus-plugin = "2.0.0-alpha-202607120312"
8+
nucleus = "2.0.0-alpha-202607120617"
9+
nucleus-plugin = "2.0.0-alpha-202607120617"
1010

1111
[libraries]
1212
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }

src/jvmMain/kotlin/dev/nucleusframework/composenativetray/tray/api/TrayApp.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import dev.nucleusframework.composenativetray.utils.isMenuBarInDarkMode
5757
import dev.nucleusframework.core.runtime.LinuxDesktopEnvironment
5858
import dev.nucleusframework.core.runtime.Platform
5959
import dev.nucleusframework.window.tao.TaoStandalonePopup
60+
import dev.nucleusframework.window.tao.isTaoStandalonePopupAvailable
6061
import kotlinx.coroutines.CoroutineScope
6162
import kotlinx.coroutines.Dispatchers
6263
import kotlinx.coroutines.delay
@@ -451,7 +452,16 @@ fun NucleusApplicationScope.TrayApp(
451452
"nucleus.decorated-window-tao module."
452453
}
453454

454-
if (Platform.Current == Platform.Windows || Platform.Current == Platform.MacOS) {
455+
// Linux gates on runtime availability: the panel is a raw X11 window,
456+
// reachable through XWayland on Wayland sessions but absent on rare
457+
// Wayland-only setups — those fall back to the opaque window impl.
458+
val panelAvailable =
459+
remember {
460+
Platform.Current == Platform.Windows ||
461+
Platform.Current == Platform.MacOS ||
462+
(Platform.Current == Platform.Linux && isTaoStandalonePopupAvailable())
463+
}
464+
if (panelAvailable) {
455465
TrayAppImplPanel(
456466
iconContent, iconRenderProperties, tooltip, state, windowSize, visibleOnStart,
457467
enterTransition, exitTransition,

0 commit comments

Comments
 (0)