Nucleus 2.6 - #628
Draft
kdroidFilter wants to merge 207 commits into
Draft
Nucleus 2.6#628kdroidFilter wants to merge 207 commits into
kdroidFilter wants to merge 207 commits into
Conversation
…tensions DSL
Add a macOS { appExtensions { } } DSL to embed prebuilt .appex bundles into
Contents/PlugIns, each signed with its own entitlements and (optional) provisioning
profile, then seal the outer app without --deep so the extension keeps its distinct
signature. Covers the JVM/jpackage path, the DMG/PKG re-seal (electron-builder), and
GraalVM native images. Adds the macos-appex-demo example.
No behavior change when appExtensions is unused (all new paths are guarded).
Tao is now the single window backend. Removes the three AWT-based modules
(`decorated-window-awt`, `-jbr`, `-jni`) with their native sources, API
dumps, detekt baselines and GraalVM metadata, plus the `jni-demo` sample.
BREAKING CHANGE: `NucleusBackend`, `LocalNucleusBackend`, the `backend =`
parameter of `nucleusApplication`, `NucleusApplicationScope.backend` and
`NucleusWindowUnsafe.awtWindow` / `awtDialog` are gone, as are the AWT
overloads of `MaterialDecoratedWindow` / `MaterialDecoratedDialog` (M2, M3)
and `JewelDecoratedWindow` / `JewelDecoratedDialog` — only the
`NucleusApplicationScope` receivers remain. Compose Desktop's AWT `Window`,
`Dialog` and `Tray` are unsupported; use `DecoratedWindow`, `HostedWindow` /
`HostedDialog` and an AWT-free tray.
- nucleus-application: drops the AWT scope/window/dialog adapters and takes
`api(project(":decorated-window-tao"))`, since the backend is no longer a
consumer choice and a missing runtime module would only fail at launch
- material2: ports `MaterialDecoratedDialog` to a `NucleusApplicationScope`
receiver — it existed only in AWT form, so M2 keeps parity with M3
- removing the AWT overloads also retires the `LowPriorityInOverloadResolution`
/ `INVISIBLE_REFERENCE` workarounds they needed
- taskbar-progress-tao: single Tao dispatch path
- core-runtime keeps `WindowBackend.Awt`: it still describes a plain Compose
Desktop / Swing host embedding Nucleus libraries
- scheduler-demo moves to Tao (drops the `java.desktop/sun.*` add-opens);
service-management-demo swaps `java.awt.EventQueue` for
`rememberCoroutineScope().launch`, as SMAppService completion handlers must
hop to the Tao main thread
- drops the now-unused `jbr-api` catalog entry, the jni/jbr native build steps
and verify entries from CI, and rewrites the backend docs
- also adds `rect-stress-demo` / `widget-demo` to `apiValidation.ignoredProjects`
alongside the other demos, fixing their pre-existing `apiCheck` failures
Compose hardcodes grayscale as the Windows font-smoothing default. Enable ClearType end to end without runtime reflection: - plugin: LcdTextDefaultTransform (artifact transform + ASM) patches FontRasterizationSettings.PlatformDefault in ui-text-desktop jars on non-test runtime classpaths — SubpixelAntiAlias on Windows, opt-outs -Dnucleus.text.lcd=false (runtime) / -Pnucleus.text.lcd.patch=false (build). Android/HotReload/KMP guards mirror the CleanNativeLibs transform; referenced ctor/enum fields are verified so Compose layout drift fails the build. Canary test patches both the plugin's Compose and the consumer version from the root version catalog. - tao: lcdSurfaceProps attaches the OS-queried pixel geometry (cached, RGB/BGR, grayscale on any unknown) to opaque Windows window surfaces only; per-pixel-alpha surfaces (popups, NativeView overlay, Mica or Acrylic backdrops, transparent windows) keep unknown geometry so Skia falls back to grayscale. renderGlFrame now requires windowTransparent. - jewel-demo: use JewelDecoratedWindow instead of hand-rolled theming.
DecoratedWindow, DecoratedDialog, HostedWindow and HostedDialog take androidx.compose.ui.window.v2 state. Requested geometry is applied asynchronously; observed bounds/placement publish once the window is shown. tao-demo uses the v2 rememberWindowState and requestPlacement path.
…text feat(tao): LCD/ClearType text on Windows
WindowState.requestSize()/requestPosition() build the two-arg WindowBoundsProvider, whose getBounds dereferences an AWT-backed WindowGeometryProviderScope. Tao has none, so the request was dropped — but the placement was already rewritten to Floating, knocking a maximized window out of maximized for a request that never applied. Skip the whole request when the provider cannot be evaluated, and log it at WARNING (rememberWindowStateWithBounds hits the same path). The observed bounds now fall back to the native window rectangle when the v1 position never turns Absolute: a WM that emits no move event for a PlatformDefault window left WindowState.isInitialized false and bounds / size / position throwing forever. Also: narrow the constantBoundsOrNull catch to NullPointerException so a provider's own failure is not reported as "needs live metrics"; move dialog size clamping out of composition into an effect; move inspectableWindowBounds to dev.nucleusframework.window.tao so apiCheck covers it and the split package with compose-ui is gone; document that setMinimumSize/setMaximumSize clear per window, not per axis; register ComposeWindowV2BridgeTest with the scene test battery drift guard.
Compose v2 documents WindowState.bounds as the whole window, insets included, but the bridge published the v1 state's outer position paired with its inner size — so bounds.size changed meaning once the WM emitted its first move event, and requestBounds(state.bounds) or a WindowState.Saver restore resized the window by the decoration insets. Observed bounds now always come from the native outer rect, and the request path converts back to the inner size the v1 state expects. Hosts that never expose the TaoWindow (rememberSyncedWindowState) left isInitialized false forever on a window manager that emits no initial move, making every bounds / size / position read throw. They now publish an approximate outer rect instead. The initial v2 -> v1 conversion drains the request channels, so a window that left and re-entered composition before ever being visible fell back to the 800x600 platform default. Memoize the drained geometry per state. constantBoundsOrNull treated any NullPointerException as "this provider needs AWT window metrics", hiding real provider bugs behind a dropped geometry request. Only the shapes that come from the null scope we pass in count now. requestSize / requestPosition stay inert: their providers live in a synthetic lambda's captures, so honouring them would need reflection, and building a WindowGeometryProviderScope would need a displayable AWT window. Add requestInspectableBounds() as the working equivalent and point the diagnostics at it.
Compose 1.12's `androidx.compose.ui.window.v2` is anchored to AWT: `Screen` wraps a `java.awt.GraphicsDevice` and reads its insets through `Toolkit.getDefaultToolkit()`, and `WindowGeometryProviderScope` takes a `java.awt.Window` that must already be displayable. The Tao backend has neither, so every provider that touches the scope was accepted, logged and dropped, and `requestScreen` was drained into the void. Mirror the package instead, member for member, as `dev.nucleusframework.window.tao.v2`, backed by our own monitor enumeration and `TaoWindow` rather than by AWT. Migrating is a single import change, and deleting the package restores the upstream import unchanged if JetBrains decouples its own types. - `TaoMonitors` / `TaoMonitor`: multi-monitor enumeration via a new `nativeGetMonitors` on each platform bridge (`EnumDisplayMonitors` + `GetDpiForMonitor` on Windows, `NSScreen.screens` on macOS, GDK monitors on Linux), one tab-separated descriptor per monitor. Physical pixels, top-left origin, work area included — the conventions the existing primary-monitor calls already used. Never reports zero monitors. - `v2`: `Screen`, `WindowScreenProvider(Scope)`, `WindowMetrics`, `WindowGeometryProviderScope`, `WindowBoundsProvider` / `WindowSizeProvider` / `WindowPositionProvider` with their companions, `WindowState`, `DialogState`, savers and `remember*` factories. - `DecoratedWindow` / `DecoratedDialog` / `HostedWindow` / `HostedDialog` / `NucleusWindowHost` overloads for the cloned states. The host default bodies fall back to the v1 surface, so themed hosts keep working; the default host overrides them for the full path. - Size and position stay split instead of folding into a `DpRect` (`CombinedBoundsProvider`): a rectangle cannot carry an unspecified position or a wrap-content axis without turning both into `NaN`. - Wrap-content sizing routes through the window's own path rather than a one-shot content measurement, so `Unconstrained` / `PreferredWidth` / `PreferredHeight` keep re-measuring. The Compose-typed overloads stay as they are — best effort with the warning — and their KDoc now points at the clone. Verified headfully on a real window (`taoHeadfulTest`, 5 new cases): initial provider centring, `requestSize` / `requestPosition`, a scoped bounds provider reading live window metrics, `requestScreen` landing on the target monitor, and `screenId` tracking the hosting monitor.
Brings the 2.6 line up to date with the released one (#629 MSI installer options, #630 NSIS menu category, #632 clean-frame present skip, #633 alwaysOnTop stickiness). Conflict: `nucleus_tao_windows_deco.c` — 2.6's ClearType pixel-geometry probe and main's #631 topmost helpers were appended at the same spot. Both kept. Verified on Windows: rebuilt natives, `check` on decorated-window-tao and nucleus-application, headful suite 27 run / 0 failed.
Observed geometry was only published from an effect keyed on the v1 state, so a move or resize the window manager applies without the v1 state changing left `WindowState.bounds` / `position` / `size` reporting a stale rectangle for the rest of the window's life. The initial geometry apply is exactly that case: it lands after the effect has already run. Bump a counter from the window's own move / resize callbacks and key the publishing effect on it too. Both binders get it — the Compose-typed one has the same shape and the same gap. Caught by the headful suite, which only reproduced it with the full case list: the filtered run happened to settle in time.
Both conflicts are additive registries where 2.6 and this branch appended at the same spot: the JVM-only test list (its LCD capture test vs our monitor / bridge tests) and the headful suite registry (AlwaysOnTopHeadfulCases vs WindowApiV2HeadfulCases). Both sides kept. Verified on Windows: rebuilt natives, `check` on both modules, headful suite 32 run / 0 failed, twice.
… API Written blind on a Windows box and caught by CI. Three mistakes, all verified this time against the gdk 0.18.2 sources: - `gtk::gdk::prelude::DisplayExt` does not exist — `Display`'s monitor accessors are inherent in gdk3-rs. This is the E0432 that failed the build. - `Monitor::is_primary()` does exist, so the primary flag no longer has to be matched on geometry — which would not have compiled either, since `gdk::Rectangle` implements no `PartialEq`. - `Rectangle` is a boxed inline type, so selecting between the geometry and the work area *by value* moved a rectangle still read afterwards. Read the four numbers out first and pick between tuples. Also spell the tab/newline sanitiser as two `replace` calls: the char-array `Pattern` impl is newer than the toolchain floor this crate builds with.
`gdk::Display::default()` is `assert_initialized_main_thread!()`, and a failed Rust assertion crossing FFI aborts: the enumeration took the whole test JVM down with SIGABRT on a headless CI box (exit 134). Guard the no-window path with `gtk::is_initialized_main_thread()` and report "no monitors" instead, so a tray-only app or a unit test gets the synthesized fallback rather than a dead process. The X11 work-area fallback behind that synthesized monitor is already headless-safe (`XOpenDisplay(NULL)` returning NULL).
`LcdTextTest > Compose LCD text on an RGB surface has chromatic edges` has been failing the macOS tao-tests job since #626 merged, which leaves every PR targeting this branch red. Skia can only fringe where the platform font host produces subpixel glyph masks: DirectWrite and FreeType do, CoreText does not — macOS dropped subpixel antialiasing in Mojave and renders grayscale whatever the surface's PixelGeometry says. So `lcdScore == grayScore` there, which is this feature's documented behaviour (`macOS and Linux stay grayscale` asserts the same thing on the surface-props side) rather than a regression. Skip the pixel assertion on macOS only; Windows and Linux keep it.
`tao-headful (ubuntu-latest)` timed out on the centring case while the four other clone cases passed — so positioning works there; it is the *initial* position that openbox overrides with its own placement policy. The v1 path retries its Aligned centring for the same reason. Split the assertion: the size and the strict centre where the platform honours the request, and containment in the target work area on Linux. Prints the observed rectangle so the CI log carries the numbers.
The native move / resize callbacks bumped a snapshot-state counter, and those callbacks run on the event-loop thread from within the platform's resize handling — which can be *inside* a Compose measure/layout pass. The recomposition that write schedules then re-entered layout: `IllegalArgumentException: performMeasureAndLayout called during measure layout`, which took down the GraalVM headful battery on all three platforms. Signal through a conflated channel instead. A send carries no snapshot obligation, and the receiving coroutine resumes on the dispatcher once the native frame has unwound, so publication happens outside the pass.
`setMenu` reaches `g_bus_get_sync(G_BUS_TYPE_SESSION, …)`, which takes no
timeout: on a runner with no session bus it blocks forever. The test task then
never finishes and hangs the whole `preMerge` job until the 30-minute cap kills
it — the failure mode pre-merge.yaml's own comment records ("`:launcher-linux:test`
has done exactly that three times"), and it just cost another PR two runs.
Skip when neither `DBUS_SESSION_BUS_ADDRESS` nor `$XDG_RUNTIME_DIR/bus` is
there: without a bus there is nothing to register against anyway.
…ow hooks Rename DecoratedDialog's applyDialogOwnerRelationship to applyWindowOwnerRelationship and add its inverse, clearWindowOwnerRelationship, so a second secondary-window archetype can reuse the Win32 / AppKit / GTK owner plumbing. TaoWindow gains what a window that observes *another* window needs: setOuterPositionPx (physical-pixel positioning, SetWindowPos on Windows so a second-monitor DPI never leaks in), remove*Listener counterparts for the multi-cast moved / resized / destroyed / fullscreen-prepare hooks, and an onClosing hook fired at the start of requestClose() so owned windows can sever their owner link before the OS would take them down with it.
Add SatelliteWindow, the floating tool-palette / inspector archetype on Tao, with a Nucleus-level overload in nucleus-application: - WindowPositioner / WindowAnchor / WindowConstraintAdjustment: pure placement geometry with a flip → slide → resize cascade, pinned by 12 unit tests (registered in the scene battery and drift test). - Anchored initial placement, parent-relative follow in physical pixels with echo filtering, offset re-capture when the user drags the satellite, suppression while the parent is fullscreen or maximized, and SatelliteWindowState.reanchor() to re-apply the rule. - Reparenting keeps the satellite where it is on screen, including when the previous owner closes in the same frame: the owner link is severed before the old window is destroyed and the close decision is taken from composition, where the new owner is already known. - Headful coverage: anchoring + follow, maximize suppression + restore, reanchor, and reparent-as-the-owner-closes. The harness gains a selectable satellite owner, a closable dialog and onCloseRequest routing for that last case. - examples/satellite-demo: two document windows sharing one inspector.
…ble (#576) The outer gate exists to catch chrome drift — TitleBar and frame disagreeing. But the outer rectangle is a separate query from the resize event the scene tracks: on a loaded Xvfb the X server's geometry lagged the scene by 3px over two consecutive samples while `maxSceneVsInner` stayed at 0, and `SUSTAINED_FRAMES = 2` promoted that into a failure. Only the outer query can see such a lag; the scene has nothing to correct. Fail on outer drift only when the scene also lost the inner size. The metric line keeps reporting it either way.
Nine of the last nine `preMerge` jobs that hit the 30-minute cap — on main as much as on feature branches, two of them running the full 6 hours before the cap existed — were stuck in `:launcher-linux:test`. The culprit is `g_bus_get_sync(G_BUS_TYPE_SESSION)` with `DBUS_SESSION_BUS_ADDRESS` unset: GDBus then autolaunches, spawning `dbus-launch --autolaunch`, which waits on an X display a headless process never provides. No timeout, so the JNI entry point never returns, and with `nativeRegisterQueryHandler` the calling thread also sits in a condvar wait for a worker thread that is itself stuck there. Refuse to connect when the address is unset: a session bus that exists is always advertised through that variable, so "unset" means "no bus", and the bridge already treats a NULL connection as "launcher unavailable". This fixes the headless-app case too — a service or CI process must not spawn dbus-launch. Guard the second native test the same way (the quicklist one already was), and drop the `$XDG_RUNTIME_DIR/bus` probe from its check: that fallback is libdbus behaviour, not GDBus's.
`requestSize` and `WindowBoundsProvider(sizeProvider = …)` pair the size with `WindowPositionProvider.Current`. Before the window exists that was resolved against the placeholder rectangle the initial scope hands out, which pinned the window to an absolute point. The v1 `rememberWindowState(size = …)` idiom this replaces leaves placement to the window manager — keep that: an initial size-only request now resolves to `WindowPosition.PlatformDefault`. Once the window is up, `Current` reads the live outer rectangle as before. Review follow-up on #634; the other points (AWT on the Tao thread, dummy peer, dialog constraints, partial min size, host fallback, KDoc) were already addressed by the clone.
The Linux headful job timed out once on the initial size converging and passed the run before with the exact requested size; the diagnostic sat after that wait, so the log had nothing. Print the outer rectangle once a second during the wait.
Two X11 facts make it say nothing there. openbox applies its own placement policy to a client's initial position (the window lands at 0,0 — the v1 path retries Aligned centring for the same reason), so the centre is never observable. And this is the only headful case whose window receives an absolute position *before* `show()`: under Xvfb/openbox that window intermittently stays at GTK's unallocated 1×1 for the whole 15 s budget — the sizing trace shows it — while the very next window of the same run maps normally. That is a pre-map race in the v1 create → move → show sequence, independent of the clone, and not reproducible from a Windows box. Size, position and screen requests after mapping stay covered on every platform by the four sibling cases.
Compose's own `androidx.compose.ui.window.v2` types are no longer accepted by `DecoratedWindow` / `DecoratedDialog` / `HostedWindow` / `HostedDialog` or the `NucleusWindowHost` / `NucleusDialogHost` surfaces. On Tao that surface could only ever be half-working — every scoped geometry provider (including the ones `requestSize` / `requestPosition` build internally) and `requestScreen` were accepted, logged and dropped, because Compose's scope needs a displayable `java.awt.Window`. An API that silently ignores part of its contract is worse than one that does not exist; the supported v2 surface is the clone, `dev.nucleusframework.window.tao.v2`, where everything is applied and migrating is one import. Removed: `ComposeWindowV2Bridge`, the `ComposeWindowV2Access` friend-package accessor, the Compose-typed `DecoratedWindow` / `DecoratedDialog` overloads, `inspectableWindowBounds` / `requestInspectableBounds`, `rememberSyncedWindowState` / `rememberSyncedDialogState`, the matching nucleus-application overloads, adapters and host methods, and their tests. The geometry helpers the clone shared with that bridge move into `NucleusWindowV2Bridge`. `examples/tao-demo` and the host tests use the clone.
Notification, launcher and media-control callbacks were posted with SwingUtilities.invokeLater. Under the Tao backend the AWT EDT is not Compose's UI thread, so those callbacks land on a thread that paints nothing — the same mistake menu-macos fixed in #310, still present in five modules. WindowBackend documents itself as the escape hatch for exactly this ("avoid touching the AWT event dispatch thread when running on Tao") and was read nowhere: its only use in the tree was the write in nucleusApplication. - add NucleusUiThread to core-runtime: a single marshalling point backed by an executor the backend registers, falling back to EventQueue.invokeLater when no Nucleus entry point ran. No new dependency, so the OS modules stay Compose-free and headless-capable. - register it (and WindowBackend.Tao, which a bare TaoApplication.run app never recorded) from TaoApplication.run, so a plain taoApplication host is covered too, not just nucleusApplication. - route the nine call sites in notification-linux, notification-windows, media-control, launcher-linux and launcher-macos through it. - restate the seven public KDoc blocks that promised the Swing EDT.
PKG was hardwired to the Mac App Store: TargetFormat.isStoreFormat forced
the sandboxed pipeline, "3rd Party Mac Developer" certificates and a
post-build productsign, so a PKG for MDM deployment or manual install
outside the store was not buildable.
Whether a PKG is a store package is now a DSL choice, macOS { pkg {
appStore } }, defaulting to the previous behaviour. With appStore = false
the PKG takes the same non-sandboxed pipeline as the DMG, electron-builder
signs the installer itself from the bare NAME (TEAMID) identity, a DSL
keychain travels as CSC_KEYCHAIN, and notarizePkg notarizes the .pkg.
electron-builder silently emits an unsigned package when no matching
"Developer ID Installer" certificate is found, so the task verifies the
result with pkgutil --check-signature.
pkg { preInstall / postInstall } stage install scripts for pkgbuild
--scripts. The App Store rejects them (error 90254), so they require
appStore = false, checked at configuration time.
The staged preinstall/postinstall are shims. electron-builder sets
BundlePre/PostInstallScriptPath *and* passes --scripts, so PackageInfo
declares each script twice and the Installer runs it twice, confirmed on
a real install. The shim skips the per-bundle pass and execs the app's
script, staged under a name electron-builder's scan does not match.
Runtime: ExecutableRuntime.isSandboxed() reads APP_SANDBOX_CONTAINER_ID.
The scheduler gates on it instead of isPkg(), and a Developer ID PKG
becomes self-updatable while the sandboxed store build stays excluded.
The ignored-projects list was maintained by hand, so every new sample had to be added to it. Twice it was not, and apiCheck failed with "Expected file with API declarations ... does not exist" for macos-appex-demo and reader-dock-demo. Excluding every :examples: subproject removes the class of failure. The predicate matches the one already used for detekt and explicitApi() lower in the file. decorated-window-jewel stays listed explicitly: it is not a sample, it is BCV's ASM being unable to read JVM 25 class files.
feat(pkg): Developer ID PKG with install scripts (#249)
ktlintCheck was failing on nucleus-2.6 independently of any branch: chain-method-continuation on the two kover plugin applications in the root build script, and an unused NucleusDecoratedWindowScope import in satellite-demo. Applied ktlintFormat to those two targets only, so the change is limited to them. detekt, ktlint and apiCheck are now green across the whole build.
… UI thread
`NotificationCenter` dispatches its delegate callbacks on a worker pool of
its own ("NucleusNotificationCallback-N"), so the portable `notification { }`
callbacks were the only ones left off the host UI thread on macOS while the
Linux and Windows bridges already post to it.
Verified on macOS with a real notification: clicking a button, the body and
the close box now all reach Kotlin on the Tao main thread (the thread Compose
composes on), instead of `NucleusNotificationCallback-1`.
`onFailed` from the `add()` completion takes the same route, since that
completion comes off the same pool.
#444) On Wayland `wl_egl_window_resize` only records a *pending* size: the buffer behind the default framebuffer is reallocated inside the next `eglSwapBuffers`. Skia's render target wraps that framebuffer (`fbId = 0`), so building it from the size we just requested overstates it for one frame, and under `SurfaceOrigin.BOTTOM_LEFT` the frame lands that many rows off the top of the real drawable — the band of clear colour the issue sees flicker on roughly a third of the frames of a drag. Both earlier attempts predicted that size rather than reading it: first "the buffer follows the request" (the flash), then "the buffer is one present behind", which had to be confined to KWin because the prediction was wrong elsewhere — it fixed Fedora Mutter and regressed Ubuntu GNOME. `eglQuerySurface` is neither prediction but the answer, so there is no desktop environment left in the decision: `useDrawableSizedPaint`, `drawableWidthPx` / `drawableHeightPx` and `onDrawablePresented` are gone. Where a driver answered with the requested size instead of the real one, this would behave exactly as the code did before it. Layout and the render target are also separated, which is what made the earlier attempt a trade-off in the first place: the scene keeps the window's size, so Compose never measures for a buffer that is a step behind, and only the render target follows the drawable. A frame drawn while the buffer lags is then anchored correctly and merely leaves that step uncovered until the catch-up frame, instead of displacing everything by it. Measured by a new headful case against a nested compositor: 16 of 64 frames painted at a size the buffer did not have before, 0 after, the window converging to its final size. The case fails when it measured nothing — it requires the window to have actually changed size, and reports the render passes dropped on a swap still in flight, because a window the compositor treats as occluded never gets its frame callbacks, renders nothing at all, and used to look exactly like a pass. `taoHeadfulTest` now forwards WAYLAND_DISPLAY so the suite can be pointed at a nested compositor instead of whichever session owns the screen. Defects (1) and (3) of the issue are untouched: the buffer still does not arrive in the same commit as the window geometry, which is a commit-ordering problem between GTK's toplevel and our sub-surface rather than a render target one.
fix: marshal native callbacks to the host UI thread, not the AWT EDT
Querying the drawable instead of predicting it is only authoritative if the driver cannot act on the pending `wl_egl_window_resize` after the answer is given. That held on Mesa, which defers the reallocation to `eglSwapBuffers` (measured: 0 of ~180 frames changed size under the frame). It does not hold on the NVIDIA proprietary driver, which reallocates when the back buffer is first used for rendering — in the middle of our frame, after the render target was built (measured on Ubuntu 26.04 / RTX 5060 Ti / 595.84: 12 of ~80 frames). So pin the moment rather than predict it per driver: on the frames that pushed a resize, bind the default framebuffer and clear it before querying, which is the first use either driver is waiting for. The answer then describes the buffer the whole frame lands in on both. The clear is not extra work — the frame clears anyway — and it is confined to resize frames because it costs a Skia GL state reset.
The render target is built from one `eglQuerySurface` taken before the frame, which is only authoritative if the driver cannot act on the pending resize afterwards. Drivers differ on exactly that, so the case reports the frames where it happened anyway and prints what the buffer became, which is what tells a stale basis from a correct one. This is what identified the NVIDIA behaviour: 12 of ~80 frames per run there, 0 of ~180 on Mesa, every one of them `queried -> requested` — Skia painting at the pre-configure size into a buffer that had already reached the new one.
Both said more than what was measured, which is the same mistake that produced the desktop-environment branch this change removes. - The paint-size KDoc claimed the queried drawable never changes under a frame, measured on Mesa and stated flatly. It changes on the NVIDIA proprietary driver; say which driver does what, and point at the call that pins it. - The headful case claimed compositors ignore `setInnerSize` "(this one does)". Mutter 50.1 honours it — #576 drives 40 distinct sizes through it. The case still drives maximize/restore, but because a client resize is advisory and a session that drops it would leave the case measuring nothing, not because compositors reject it.
…e-sized-paint fix(tao/linux): paint at the drawable's real size, not a predicted one (#444)
The badge a Caps Lock bound to keyboard-layout switching raises is a window HIToolbox creates *inside our own process*, positioned from whatever `firstRectForCharacterRange:` answers. TaoView answered a process-global caret rect that nothing ever invalidated, so once a focused text field was destroyed the badge kept appearing over the spot the field used to occupy. Before any field had been focused it was worse: the overrides were only installed when the first text-input session started, so tao's own implementation answered — the content-rect corner with a *top-down* y handed back as a Cocoa bottom-up coordinate — which parks the badge in the bottom-left corner of the screen of an app that has never shown a text field. Three changes, all measured against a logging `NSTextInputClient` probe driven by `TISSelectInputSource` with the badge window tracked through `CGWindowListCopyWindowInfo`: The cached rect is now scoped to the view that pushed it, and every other answer is `NSZeroRect`. That exact shape is the one AppKit reads as "no insertion point": a zero *size* alone does not suppress the badge — `(0, 30, 0x0)`, which is what tao answers, still draws it — and `selectedRange = NSNotFound` plus `invalidateCharacterCoordinates` change nothing at all. A rect falling outside the key window is not drawn either, which is why the corner case only shows on a window large enough to contain it. The overrides are installed per window creation rather than on the first session, so the answer is ours from the first frame. `TaoView` only exists once a window has been built, and the swizzle is idempotent. The input context is deactivated when the session ends. On its own that is not enough — `interpretKeyEvents:` re-activates it on the next keystroke — but it takes the badge down immediately for an app that is not being typed into. The teardown carries the activation token it was handed, because focus moving between fields starts the incoming session *before* the outgoing one is torn down: without the guard, focusing a second field deactivates the context the first teardown then finds live. Same ordering trap as the document cache.
`MacOsTextInputClientProbe.imeRect` reads what TaoView answers AppKit and reports an all-zero rect as "no insertion point", which is the property both cases assert. `caretRectDiesWithTheFocusedField` walks one field lifecycle: the caret is published, it follows focus to a second field (the ordering guard — without it the run takes three times as long because the incoming session's context is deactivated and has to be re-established), it is dropped when the fields are destroyed, a keystroke does not republish it, and a field composed again gets it back. `noCaretRectBeforeAnyField` covers the window that never shows one. Both fail on the code before this branch.
…indicator fix(tao/macos): keep the input-source indicator off a caret that is gone
AppKit fires `mouseExited:` for a cursor that never left the view. Measured on 26.5 with the pointer parked over a tab strip: enter → exit → enter → exit, all at one screen point. Two sources found — a hover card's own popup panel rising over the pointer, which is a *child window* of ours whose draw margin overlaps the tab it hangs off, and the rebuilds of tao's legacy tracking rect. That reaches Compose as `PointerEventType.Exit` and is taken at face value, so hover state is dropped. Nothing corrects it: `CursorEntered` carries no position and was never forwarded, and a pointer that *rests* sends nothing more. Hover effects and tab hover cards stay dead until the user moves the mouse — the symptom the standing HACK comment in `TaoComposeSceneHost` describes as "hover doesn't render until the user clicks once". A hover card is worse than dead: it dies on its own phantom exit, reopens, and exits again, a loop no pointer can break. So trust the geometry over the event. An exit is ignored when the cursor is demonstrably still ours — inside the view's bounds, and the window on top at that screen point is this window or one of its `childWindows` — and the position is re-published instead; `CursorLeft` is kept for a cursor that really is somewhere else. `mouseEntered:` publishes its position for the same reason.
`a popup outside the owner window is left alone while the screen has room` needs the popup to land outside the window *and* inside the work area at once. The default 800 dp window centred on the runner's 1024 px display leaves 112 px for it, so the popup was clamped back in — on the case whose whole point is that nothing clamps. A small window against the left edge has room anywhere we run. The two cases that need a window *above* the work area are asking for a state macOS does not have. Measured on 26.5: `setFrameOrigin:` and `setFrame:display:` both pull a frame whose top would go under the menu bar back down, titled and borderless alike, and only an override of `constrainFrameRect:toScreen:` escapes — not a trade Nucleus makes, since AppKit runs that constraint on display changes too and a window it no longer keeps on screen is a window the user cannot reach. A user cannot drag a window off the top there either. They skip on macOS with that reason and stay covered on Windows and Linux, where the drag is an everyday gesture.
`Robot.mouseMove` warps the cursor on macOS, and the events that follow a warp carry the *pre-warp* location for a few hundred ms — measured as ten consecutive moves all reporting one stale point. A press sent inside that window is hit-tested where the pointer used to be, so a case that clicks straight after moving clicks the wrong thing. `robotPressAndDrag` now lands on its start point in two hops, the second a real move from the cursor's new home, which is what flushes the true location through. Same shape as the pattern `NativePopupMarginInputHeadfulCases` already used for its pointer cases.
…r it (#444) Two ordering defects made the content trail the window by one configure on every commit of a Wayland resize — visible as the jump on a left/top edge drag, where the origin moves a step ahead of the content. 1. Tao's GTK `draw` and `configure-event` handlers only post to its event channel: `RedrawRequested` / `Resized` reach the host after GDK's after-paint has already committed the toplevel with the new `set_window_geometry`. The frame for configure N could never ride commit N. The widget helper now connects a real `draw` handler on the GtkWindow (`nativeConnectToplevelDraw`); during a resize burst the host renders from it, taking the size from `gtk_window_get_size` (GTK's own configure-event lags the same way), with the content sub-surface in `set_sync` and the swap at interval 0, and waits for the swap before returning — so the buffer is cached compositor-side when GTK's commit applies it, atomically with the geometry. Sync is armed only from an idle swap thread and only with the interval-0 burst, or a commit with a frame callback attached would wait for the very GTK commit the draw has yet to return to. The swap interval is now applied by the swap thread before its present, so it is in force before the first synced commit. 2. Mesa's `wl_egl_window` resize callback adopts the new size only while no back buffer is acquired, and `eglMakeCurrent` acquires one: a resize pushed after it lands in the next frame's buffer, and this frame paints the previous size. `applyPendingNativeResize` now runs before `nativeMakeCurrent`; the Skia surface rebuild a scale change asks for is deferred to when the context is current. Measured with a compositor-driven left-edge resize (KWin scripting, 30 steps of 8 px) and screen capture, counting frames where the content's right edge moves — which it never does for a plain GTK3 window: before 64–82 of ~300 frames, after 0 on three runs. A WAYLAND_DEBUG trace pairs every GTK geometry commit with a child buffer of exactly that width (100/100).
…arded A native popup layer hands its parent every mouse event that lands in the margin it draws its shadow in. AppKit, though, keeps a whole button gesture on the window that took its mouseDown — this panel — so the parent was being given a press whose end it could never see. Two ways to lose it, both measured on a tab strip's hover card: the region hit-test answers differently on the way up (the press is what dismisses the card, which re-lays out the content under the pointer), or the panel is ordered out between the two, and an ordered-out window receives nothing at all. The release then reaches no one: not the panel, not the owner's view, not the JVM. What the owner is left with is a press that never ends. Compose holds the gesture open, so the click it belonged to never happens — the tab under the pointer is not selected — and the next press is read against the stale state (`TaoComposeSceneHost` closes it out there, which is why a second click appeared to work). So the panel now tracks which buttons it forwarded: the rest of that gesture follows its press to the parent whatever the region test says, and a panel ordered out still holding one hands over the release AppKit will not deliver.
The two appearance comparisons failed intermittently on a real display, on metrics that read one frame each. `firstVisible` — and `slideIn`, measured from it — anchored on the first frame where the colour probe caught the dialog. The dialog fades in over the scrim, so that frame sits on the knife-edge of the detection threshold: it came out bimodal on *both* layers, 0 ms on the runs that caught the faint start and ~60 ms on the runs that did not, and the comparison failed whenever the two films happened to land in different modes. They now anchor half-way through the fade, far from that edge and the same moment of the same animation either way. `hideMinHeightRatio` took the strict minimum over the fade-out. What it guards against is a surface that *stays* collapsed for the whole fade; a single short frame is a drawable caught mid-present, which an OS surface can show and a scene drawing into the window canvas never can (measured: 1 px, and half the dialog, in runs whose neighbouring frames were both full height). Read over two consecutive frames it keeps the guard and drops the compositor.
…frame-ordering fix(tao/linux): commit the resize frame with GTK's geometry, not after it (#444)
…mized A maximized, tiled or fullscreen window has no CSD shadow ring, so the content sub-surface's opaque region covers the toplevel edge to edge. Mutter culls such a parent as obscured and stops answering its `wl_surface.frame`; GDK freezes its frame clock on the unanswered callback of the last commit GTK made in that state. Two symptoms followed the #444 in-frame path: 1. The whole UI froze on maximize. The burst rendered only from GTK's `draw`, and its end was only evaluated inside the render pass — with GTK unable to paint, no frame was ever rendered again, and the coroutine continuations drained there never resumed. The burst's end (`endResizeBurstIfStale`) now runs from `onRedrawRequested` too, every `queue_draw` is watched (`askToplevelDraw`, 50 ms grace + a watchdog redraw through `DelayScheduler`) and an unanswered one drops the burst back to the event-loop path; the in-frame path is never armed while the window is maximized / tiled / fullscreen (`parentObscured()`). 2. Hover and drags were choppy while maximized although the app rendered at full rate. GDK3 holds a lone motion event until the frame clock's flush-events phase, which does not run while the clock is frozen, so motion was only delivered when another event arrived. GTK repaints the toplevel once after every maximize (`applyContentOffset`, to land the sub-surface at (0, 0)), and that commit went unanswered. The content's opaque region now always leaves its bottom row out, so the compositor keeps painting the toplevel and its callbacks keep coming. Measured with WAYLAND_DEBUG over 9 maximize/restore toggles: every one of the parent's 67 frame callbacks answered, max latency 25 ms (before: 2.5 s or never), content at 90 fps throughout, no protocol gap over 22 ms.
A tag `v<semver>-dev-<id>` (convention `v2.6.0-dev-YYYYMMDDHHMM`, cut by the
`tag-dev` skill) now publishes the runtime modules to Maven Central and the
plugin to the Gradle Plugin Portal without running `preMerge` — the usual
Kotlin-ecosystem dev build, so a downstream app can consume 2.6 before it is
released. Natives are still built and verified; the JARs would be unusable
otherwise. Nothing else in the publish graph runs tests: `publishToMavenLocal`
pulls in `compileKotlin` → `jar`/`sourcesJar`/`javadoc` → `pom`/`publish` and
neither `test`, `apiCheck` nor `detekt`.
`release-tag-info` is the single place that classifies a tag. It also rejects
anything that is not `v<semver>`: every module derives its version with
`GITHUB_REF.removePrefix("refs/tags/v")`, so a bare `dev-2026…` tag would have
published a version literally named `refs/tags/dev-2026…` — permanently, on
Central.
Dev tags are excluded from `release-desktop` / `release-graalvm`: they publish
libraries, they should cut no GitHub release and burn no packaging matrix.
`validate-release-ref` now derives the branch a prerelease tag must live on from
the tag itself (`v2.6.0-rc.1` → `nucleus-2.6`). The pinned default was still
`nucleus-2.0`, a branch that no longer exists on origin, so the next rc would
have failed to fetch — the guard was protecting the previous release line.
Merged
8 tasks
kdroidFilter
added a commit
to NucleusFramework/EdgeTranslator
that referenced
this pull request
Sep 20, 2026
Kotlin 2.4.20, Nucleus 2.6.0-dev, plus ktor, metro, sqlDelight, filekit, materialKolor, activity-compose, compose-rules, stability-analyzer and aboutLibraries. Nucleus 2.6 (NucleusFramework/Nucleus#628) retires the AWT/JBR/JNI window backends, so NucleusWindowUnsafe.awtWindow is gone and the AWT fallback in fileKitDialog() no longer compiles. It ships in the same commit to keep the tree buildable. The XDG portal path is unchanged. Windows now parents the picker to TaoWindow.nativeHandle (the HWND) via FileKitDialogParent.windows, which is what that property's KDoc points at. macOS gets an unparented dialog: nsWindowHandle is an NSWindow* and FileKit has no macos() factory yet. OS detection goes through Nucleus' own Platform.Current.
kdroidFilter
added a commit
to NucleusFramework/EdgeTranslator
that referenced
this pull request
Sep 20, 2026
* chore: bump dependencies and adapt to the Tao-only Nucleus 2.6 Kotlin 2.4.20, Nucleus 2.6.0-dev, plus ktor, metro, sqlDelight, filekit, materialKolor, activity-compose, compose-rules, stability-analyzer and aboutLibraries. Nucleus 2.6 (NucleusFramework/Nucleus#628) retires the AWT/JBR/JNI window backends, so NucleusWindowUnsafe.awtWindow is gone and the AWT fallback in fileKitDialog() no longer compiles. It ships in the same commit to keep the tree buildable. The XDG portal path is unchanged. Windows now parents the picker to TaoWindow.nativeHandle (the HWND) via FileKitDialogParent.windows, which is what that property's KDoc points at. macOS gets an unparented dialog: nsWindowHandle is an NSWindow* and FileKit has no macos() factory yet. OS detection goes through Nucleus' own Platform.Current. * fix: unpin LiteRT-LM and move to 0.17.1 0.14.0 was pinned because 0.15.0/0.16.x aborted the JVM on Windows CPU generate (0xC0000409 in litertlm_jni.dll nativeGenerateContent / sendMessage, LiteRT-LM#3230). 0.17.1 fixes it. ThinkingConfig and maxOutputToken were commented out only because 0.14.0 predates them; both come back on JVM and Android. LiteRtWindowsSmokeTest covers the three paths that used to abort — CPU, GPU, GPU with speculative decoding. It needs a real .litertlm, so it skips unless -Dlitertlm.test.model (or LITERTLM_TEST_MODEL) points at one. A native abort takes the test JVM down, which Gradle reports as a crashed worker — that is the signal we want. * feat: enable multi-token prediction by default MTP is roughly twice as fast on GPU and now holds up on Windows with LiteRT-LM 0.17.1, so it becomes the default instead of an opt-in. Snapshots written before the flag existed inherit the new default; an explicit "off" is still preserved. The settings copy drops "experimental" and opens with "on by default" across all 35 locales, with gender agreement where the subject noun requires it. * style: clear all ktlint and detekt violations Mostly ktlintFormat output: import ordering, blank lines between multiline when-branches, one argument per line, missing braces, a default Modifier on ThemedScrollbar. Two findings needed a real fix: - TwoPane reused its first/second slots across the Column and Row branches, so crossing the 680.dp threshold discarded each pane's text, cursor and scroll state. Both slots are movableContentOf now. - LocalMicLevels is a deliberate CompositionLocal: the level flow is provided once at the root and collected only in the two waveform composables, which keeps a 60 Hz stream out of every intermediate signature. Added to the ktlint and detekt allowlists that already exist for the other app-owned locals. * feat: spellcheck the translation source field Wraps the source BasicTextField in Nucleus' SpellcheckContextMenu: red wavy underlines and suggestions in the context menu. The proofread screen is left alone on purpose — it already corrects the text itself. SpellcheckContextMenu is JVM-only, so it goes behind an expect/actual Spellchecked(); Android is a passthrough since the IME already does this. The language tag passed is the selected source language, not the UI locale: SpellChecker.locale follows Locale.getDefault(), which the app points at the interface language, so German input would otherwise be checked against a French dictionary. Auto-detect has no dictionary to pick and falls back to the process locale. * chore: refresh the composable stability reference shared.stability had not been regenerated since 189eda8, so stabilityCheck was already failing on main with 23 drifts before this branch. The bumps here and the new Spellchecked composable add a few more. The drift is mechanical — composables added, removed or moved between files, parameter counts shifting with the Compose and analyzer bumps — so the reference is simply redumped. * build: order stabilityCheck after the Kotlin compile tasks stability-analyzer 0.14.0 gives every Kotlin compile task its own build/stability/<task> directory, which the check and dump tasks read as a whole. Gradle flags that as an undeclared dependency the moment a compile task shares the graph, so `./gradlew stabilityCheck jvmTest` failed at configuration time while either task alone was fine. Ordering is all these tasks need — they only read what the compilation has already written.
Brings in #692 (macOS GraalVM Developer ID signing / notarization). Conflict in AbstractElectronBuilderPackageTask.resignApp(): 2.6 re-signs the embedded app extensions there, main re-signs the GraalVM dylibs under Contents/MacOS/. Both kept — Contents/PlugIns and Contents/MacOS do not overlap. The MacOS walk goes first so the app-extension block stays adjacent to the final bundle seal, as its comment requires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The edge double-click zoom (`-[NSWindow _zoomToScreenEdge:]`), the Window-menu tiling and `zoom:` all end in AppKit's `setFrame:display:animate:YES` — a blocking animator whose private run-loop mode services no tao observer, so every step's `Resized` sat in tao's queue until the animation had ended and the content snapped into the final bounds: the #576 trailing, one path over. `TaoWindow` now overrides `setFrame:display:animate:` and routes every animated frame change to `util::animate_frame`, the stepper #678 wrote for `set_maximized_async`, which now simply calls `setFrame:display:YES animate:YES` (the resizable and non-resizable branches merge). Guarded by `in_fullscreen_transition` / `fullscreen` so the AppKit fullscreen transition (#327) is untouched. `window_delegate::shared_state_of` reaches the `SharedState` from the window class. Headful gate `#576 AppKit frame animation (edge double-click zoom) dispatches every step in time` drives the same AppKit path programmatically (`setResizable(false)` + maximize) and asserts that every `Resized` is dispatched while the native frame is at its size: 42 events up to 1880 px off before, 41 events at 0 px after.
…-576 fix(tao/macos): step AppKit's own frame animations too — edge double-click zoom (#576)
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nucleus 2.6 vs current
main. Tao becomes the only window backend. Two multi-window archetypes land on it: satellite palettes that dock, and Chrome-like tabs. Compose 1.12's window API v2 is supported through an AWT-free clone. Windows can drop their minimize and maximize buttons.nucleusApplicationgainsexitProcessOnExit. Packaging gains macOS.appexembedding, a startup optimization pack,lastJdk, and two jlink / App Store PKG fixes. Native libraries can extract to an app-chosen cache.FsWatchershares one native watcher per instance and delivers macOS renames asMoved.The satellite / dock / tab family is
@ExperimentalNucleusApiand may still move.Breaking: Tao is the only backend
#593 deletes
decorated-window-awt,decorated-window-jbr,decorated-window-jni, andexamples/jni-demo(~10k lines).nucleus-applicationnowapi-depends ondecorated-window-tao: the backend is no longer a consumer choice, and a missing runtime would otherwise only fail at launch.Gone from the public API:
NucleusBackend,LocalNucleusBackend,nucleusApplication(backend = …),NucleusApplicationScope.backendNucleusWindowUnsafe.awtWindow/awtDialogMaterialDecoratedWindow/MaterialDecoratedDialog(M2 + M3) andJewelDecoratedWindow/JewelDecoratedDialog— only theNucleusApplicationScopereceivers remainCompose Desktop's AWT
Window,DialogandTrayare unsupported. UseDecoratedWindow,HostedWindow/HostedDialog, and an AWT-free tray.core-runtimestill hasWindowBackend.Awt: it describes a plain Compose Desktop / Swing host that embeds Nucleus libraries. The plugin'sjbr-apiProGuard keep and AOT unsealing stay, because an app can still shipjbr-apiitself.material2's dialog existed only in AWT form; it is now aNucleusApplicationScopereceiver, so M2 keeps parity with M3. Dropping the AWT overloads also retires theLowPriorityInOverloadResolution/INVISIBLE_REFERENCEworkarounds they needed.NucleusWindowHost.Windowis a publicfun interface.minimizable/maximizableare new parameters on it (and on everyDecoratedWindow/HostedWindowoverload). Themed hosts that implement the interface have to take them; 2.6 already breaks that ABI.Compose window API v2
#634. Compose 1.12's
androidx.compose.ui.window.v2is wired to AWT:Screenwraps aGraphicsDevice,WindowGeometryProviderScopetakes a displayablejava.awt.Window. Neither exists on Tao, and reflection is off the table in native-image. Accepting those types would mean every scoped geometry provider andrequestScreensilently dropped. That is worse than no surface, so Compose's v2 types are not accepted anywhere.The supported surface is
dev.nucleusframework.window.tao.v2, a member-for-member clone (Screen,WindowState,DialogState, the three providers, savers,remember*factories) backed byTaoMonitors+TaoWindow. Migrating from the Compose package is one import; deleting the clone restores the upstream import if JetBrains ever decouples its types.DecoratedWindow/DecoratedDialog/HostedWindow/HostedDialoggain overloads for the cloned states (no defaultstate, so v1 call sites stay unambiguous). Every request is applied,requestScreenincluded. Observedbounds/screenId/placement/isMinimizedrepublish from the native window on move and resize.TaoMonitorsenumerates displays without AWT (EnumDisplayMonitors/NSScreen.screens/ GDK): physical px, top-left origin, work area, never empty. Linux enumeration is checked against the real gdk3 API and never aborts the process.Size and position stay split (
CombinedBoundsProvider). ADpRectcannot carry "let the WM place it" or a wrap-content axis without turning both intoNaN. Wrap-content providers (Unconstrained,PreferredWidth/Height) re-measure continuously through the window's own wrap-content path.Wrap-content placement (#674, #675, fixes #546). A window or dialog with
Dp.Unspecifiedon an axis used to render at the right size and then sit wherever the creation fallback (800×600, or 0 dp) had been aligned. The initialAlignedis now remembered, skipped while wrap-content is unsettled, and re-applied aftersetInnerSizeonce the resize echo lands. A dialog with a parent recentres on that parent (macOS included); a parentless dialog centres on the screen, as AWT'ssetLocationRelativeTo(null). Once the measured size has landed, the scene column flips fromwrapContentWidth(unbounded = true)tofillMaxSize(), so aTitleBarspans the window instead of measuring 0 px wide.Known limits:
measureWindowContentbefore the window exists has no scene; it falls back to the current content size clamped to the constraints, then becomes a realComposeScene.measureContentpass once mapped.zoom:itself (that re-zoomed the window); setting the frame un-zooms, then bounds are applied and confirmed. A placement-toggle storm followed by a bounds request now converges instead of leaving the window zoomed (#647).Satellites, docking, Chrome-like tabs
#635 then #663. Two archetypes on one shared core, declared from
nucleusApplicationso an app never has to reach fordecorated-window-taotypes.Both families are
@ExperimentalNucleusApi(dev.nucleusframework.window, lives indecorated-window-core). Opt-in level is ERROR. Library modules and the demos opt in module-wide; a new public satellite / dock / tab declaration must carry the marker.Shared core (
window/tao/workspace/)Internal, not public:
WindowGroupRelocatedContentHost+RelocatingSaveableStateRegistryrememberSaveablestate that follows content between windowsHostGeometryCrossWindowDragDragGhostWindowScreenPlacementTaoWindow.canPlaceOnScreen— the native-Wayland gateTransferDragNative Wayland
xdg-shell gives a client neither its windows' screen position nor a way to place them. GDK reports every toplevel at
(0, 0)and ignores moves. Anything that treatsouterBoundsPx()'s origin as a screen coordinate must checkTaoWindow.canPlaceOnScreen(branch on that, not onisNativeWaylandSurface). The size half stays valid.Where the app cannot place its windows, the gesture rides the platform drag-and-drop session — the only pointer grab that crosses windows with coordinates. The source starts a session carrying an in-process token (
TaoPrivateTransfer,SAME_APPonly). The window under the pointer resolves the drop in its own coordinates and records it on the session; the source acts on that record when the session ends. Roles are inverted versus the pointer path, because the source is told nothing about where the pointer is. The drag icon is a reduced snapshot of the dragged palette or panel (TaoWindow.contentSnapshot).DragGhostWindow(popupFor = source)is the preview that follows the pointer out of a compositor-placed window (wl_subsurface, parent-relative positions).Chromium falls back to the same design on compositors without
xdg-toplevel-drag, which GTK3 cannot reach.NUCLEUS_TAO_LINUX_RENDERER=x11restores the window-following gesture.SatelliteScope.isCompositorPlacedis the same answer for the window the chrome is composed in.SatelliteWorkspace.dragKind(Window/Transfer) says how a drag in flight is carried, which is what tells preview code whetherdragGhostwill ever be published.Satellite windows
A satellite is a real native window that belongs to another one: it follows its parent, stays above it, hides with it, and can be reparented.
SatelliteWindow+SatelliteWindowStatecover the palette-attached-to-a-document case (WindowPositionerresolves the anchor).SatelliteWorkspaceis the workspace a tool app has: one palette serves whichever document is in front, and can be pulled into the document itself.nucleusApplication(args) { val workspace = rememberSatelliteWorkspace() DecoratedWindow(onCloseRequest = ::exitApplication) { JoinSatelliteWorkspace(workspace) WindowScaffold(titleBar = { TitleBar { Text("Document") } }) { padding -> DockLayout(workspace, Modifier.padding(padding)) { Document() } } } Satellite(workspace, id = "tools", title = "Tools") { ToolsPanel() } Satellite( workspace, id = "colors", title = "Colors", initialPlacement = SatellitePlacement.Docked(DockSide.Right), ) { ColorPanel() } }pinTo. When it closes, the next member takes over and the satellites move on without shifting on screen.SatellitePlacement.Floating/Docked(side)chooses between an owned window and a panel inside the owner'sDockLayout.Modifier.satelliteDragHandledrags a satellite between the two. A panel dragged out is previewed by a borderless click-through ghost window.snapshot()/restore()hand the whole layout to the app to persist.rememberSaveablestate inside a satellite survives the move between hosts.Absoluteposition is applied before the window is shown, so a satellite no longer flashes at the WM's default spot.maximizable = false. A maximized satellite breaks anchoring and the drag-to-dock hit-test. On Linux, tao'sset_maximizableis a no-op, so Super+Up is undone immediately.On Wayland, a floating satellite's header carries the drag while a caption strip beside the window controls keeps the compositor's move — the split Chrome's tab strip and GIMP's dock tabs both land on.
SatelliteCaptionStripWidth+ thefloatingCaptionslot ofSatelliteare composed only whereisCompositorPlaced, so an app never has to guess a width or accidentally claim the only area that can move the palette.Dock layout
DockLayoutis the dock. Sides nest insideOrder(outermost first, defaultDefaultDockSideOrder= top, bottom, left, right — notDockSide.entries, whose declaration order is left, right, top, bottom). A side is either split (panels share its length byDocked.weightand its thickness bydockExtent(side)) or layered (layeredSides: each panel a full-length layer of its ownDocked.extent, the shape of a nested split-pane tree without the tree).SatellitePlacement.Dockedcarries its ownextentandweight; both ride inSatelliteLayoutSnapshotand are driven bySatelliteWorkspace.setDockedExtent/setDockedWeight. Extents are fitted proportionally when the window is too small; the stored values come back with the room.The
splitterandpanelslots hand the drawing to the caller.DockSplitterScope.dockSplitterHandle()carries the gesture, so a 1 dp divider with a wider overflowing grip works.DefaultSatelliteHeaderno longer imposes a height or a background on a docked panel.Sides are physical. The layout forces LTR internally, then restores the caller's direction for content, panels and slots, so
DockSide.Leftis the left of the screen in an RTL app and the splitters drag the right way.Every panel and the content are
movableContentOf: no layout change (extent, weight, order, side, restore, side order, direction) rebuilds a subtree, so a docked pane keeps its scroll position and itsremembers. Inputs live inDockLayoutStateas snapshot state because the bands are separate composables that strong skipping would otherwise skip.Drop feedback is the target.
DockZoneHintsdraws rectangles and publishes them toHostGeometry.zoneBoundsInWindowPx.dockTargetAtresolves a drop against those, not against the window's edges. On a layered side the strip is inset behind the existing layers; the window's own edge behind them is nothing. A zone is entered when the dragged satellite's edge is within one zone thickness of the zone's outer edge and overlaps it across the other axis (edge alignment, not overlap, or a full-height panel could never be torn out). The pointer inside a zone is a second trigger and the tie-break.dockSides(Satellite(dockSides = …), default all four, empty = floating-only) is fixed at declaration and enforced everywhere:dock()andrestore()refuse another side, hints neither draw nor publish it, drag sessions filter on it, and the default header hides its Dock action for a floating-only palette.floatable = falseis a fixed panel:undock()refuses it, arestore()that floats it is ignored, the docked drag publishes no tear-out ghost, a release off every zone leaves it in place, the default header drops its Float action, and the declaration requires a dockedinitialPlacement.reorderable = falsepins the rank:dock(order)is ignored (it takes the declared rank back),insertInStackpushes any other panel past the last pinned one, drop slots keep the forbidden ranks as empty so a slot's index is still its rank, andsatelliteDragHandleis inert when a drag could not end anywhere.Ranks.
Docked.orderis kept contiguous from 0 per (host, side).dock(order)inserts at that index;nullis the rank the entry last held on that side (remembered inSatelliteEntry.dockMemory), else the end. A side with panels publishesDockDropZone.slots— one rect per rank, cut at the neighbours' centres, the dragged panel excluded — soDockTarget.orderis the rank under the pointer. A pointer over a stack beats a strip across its corner.dock()and the preview share one width (dockSeedExtent) and one weight (dockSeedWeight), so what lights up is what the release produces.Chrome-like tabs
Tabs are declared once with
Tab.TabWindowscomposes oneDecoratedWindowper group. Windows follow the tabs: a tear-off adds one, the last tab out closes one.nucleusApplication(args) { val workspace = rememberTabWorkspace() TabWindows(workspace, onLastWindowClosed = ::exitApplication) for (document in documents) { Tab(workspace, id = document.id, title = document.name) { Editor(document) } } }TabWindowshas two app slots, composed at one call site for every window, so a tab change neither rebuilds them nor moves the body's relocation keys:windowWrapperwraps the whole window including its strip (per-window locals, background).windowBodyWrapperwraps only what is under the strip — where window-level chrome goes (aDockLayout, activity bars).Two drag paths. Where the app places its windows, the gesture is
screenDragHandle→TabWorkspace.beginDrag(ghost window, screen hit-test, tear-off). A strip the card has reached counts as entered; the pointer's own strip still wins. Where it cannot (native Wayland), the grip istabStripLocalDragHandle: a local reorder driven by the pointer's travel in window px, and the moment the pointer leaves the strip the gesture is handed to the platform DnD session (transferDragHandle). That handover is what gives every other window the pointer in its own coordinates. The tab slot carriesnoWindowDrag(): the title bar's move is a compositor grab that swallows the gesture.TabStripmotion is a port ofsh.calvin.reorderable'sReorderableRowstate machine. Items arekeyed on the tab id. A tab dragged along its own strip publishes no ghost; the strip draws it at the pointer's travel since the grab. A neighbour slides one tab-width aside (springStiffnessMediumLow) when the carried tab's edge crosses its centre. On release the session setspendingReorderinstead of reordering — the strip'sTabStripMotion.settleslides the tab into the target slot, thenreorder()+rest()in the same frame, so nothing jumps. Offsets are draw-timegraphicsLayertranslations, sotabSlotgeometry is always the settled layout. Tabs open/close by width (AnimatedVisibility, 200 ms). A hover card shows the tab under the pointer; a click a drag no longer swallows.RTL is inferred from the slots.
insertionIndexis direction-aware (a right-to-left strip used to resolve every drop mirrored).One drop preview everywhere
The card that follows the pointer (
SatelliteGhostCard/TabGhostCardonDragPreviewSurface) is also drawn on the space the release fills. The dock draws it atDockLayoutState.dropRectPx(empty side: the edge strip; layered: the layer at that rank; split: the share the re-divided weights give it). The tab strip opens a slot of the dragged tab's width (TabStripScope.dropGhost). The sides merely on offer are the same surface athintintensity. No insertion bars, no drop-indicator lines. A custom strip drawsdropGhostitself, asjewel-tabs-demodoes with a placeholderTabData.Editor.Pointer icons
Compose still only defines Default / Text / Hand / Crosshair in common code, and AWT-backed
PointerIcon(Cursor(…))is unusable on Tao.TaoPointerIconsadds Grab / Grabbing, Move, NotAllowed, Wait, Progress, Help and the two axis resizes as plainPointerIconinstances carrying a native cursor code.Window chrome
#680 (fixes #504) then #684.
DecoratedWindow/HostedWindow/NucleusWindowHost.Windowgainminimizableandmaximizable, both defaulttrue, next toresizable.resizable = falsealready removed the maximize slot; this is the other half of the caption, so a login screen can be close-only:Both flags are snapshot-backed on
TaoWindowand re-applied at runtime through the sameLaunchedEffectasresizable. Restore stays available on a non-maximizable window, so one the WM maximized anyway can still leave.minimizable = falsemaximizable = falseNSWindowStyleMaskMiniaturizablecleared (Cmd+M and the Window menu follow)WS_MINIMIZEBOXdropped (taskbar click, Win+↓, system menu)WS_MAXIMIZEBOXdroppedset_minimizableis a no-op, so Super+H / Alt+Space still iconifyset_maximizableis a no-op; a satellite undoes Super+Up itselfDecoratedDialogpasses bothfalse.SatelliteWindowand workspace palettes passmaximizable = false.LCD / ClearType on Windows
#626. Compose Desktop hardcodes grayscale as the Windows font-smoothing default (CMP-5359 / compose-multiplatform#875). ClearType is enabled end to end with no runtime reflection, under HotSpot, ProGuard, and GraalVM native-image.
LcdTextDefaultTransform(artifact transform + ASM) patchesFontRasterizationSettings.PlatformDefaultinui-text-desktopjars on non-test runtime classpaths —SubpixelAntiAliason Windows. Compose layout drift fails the build: the patcher verifies the getter, the constructor descriptor, and the enum fields it references.lcdSurfacePropsattaches the OS-queried pixel geometry (cached, RGB/BGR, grayscale on any unknown) to opaque Windows window surfaces only. Per-pixel-alpha surfaces (popups, NativeView overlay, Mica / Acrylic, transparent windows) keep unknown geometry so Skia falls back to grayscale.Opt-outs:
-Dnucleus.text.lcd=false(runtime) /-Pnucleus.text.lcd.patch=false(build, skips the transform).Popup layers against the screen
#651, fixes #569.
nativePopupLayersalready exists onmain. What 2.6 changes is that those layers behave like the OS surfaces they are.LocalWindowInfosoPopup.skiko.ktsees the work-area-sized box the design always intended, instead of the owner window.popupScreenClampOffsetclamps the native frame into the work area of the display it lands on, re-clamped on every push, so an open popup survives an owner drag across monitors. Dialogs must not follow the display: layers report the window size for dialogs and the work area for popups, discriminated onscrimColor.boundsInWindowso shadows and the slide-in are not clipped. A closing dialog keeps its surface where it was while it fades out (Dialog.skiko.ktreports a zero-sizeboundsInWindowat the window centre during the fade).nativePopupLayersis now onJewelDecoratedWindow, which had no such parameter.Native context menu flyout (opt-in per popup).
NativePopupLayers { }is new: it provides, for its subtree only, the window scene's ownLocalComposeSceneContextwithcreateLayerrouted to the native popup layer factory. A friend-package Java accessor (TaoComposeSceneContextAccess) reaches that internal local without reflection. The Windows and Linux flyouts wrap in it; macOS stays onNSMenu.On Linux the menu maps as an
xdg_popupinstead of awl_subsurface, so the compositor flips and slides it. One popup per parent takes that path; dialogs keep the subsurface. A second right click now moves the menu instead of only closing it. The flyout draws real CSS box-shadow layers with each OS's own declarations (libadwaita, Breeze, WinUIMenuFlyoutatTranslation.Z = 32), becauseModifier.shadowmultiplied the OS alphas by Compose's elevation factors and left a ~1 % darkening nobody could see. The Fluent menu is laid out as WinUI lays out aMenuFlyout.macOS trackpad
#661, port of #656 (#652–#654). Scroll deltas are AWT-shaped:
preciseWheelRotation, no display scale, no extra X flip on a horizontal swipe.Trackpad gestures reach Compose as
PanStart/PanMove/PanEnd(panOffset= AWT delta × 10 dp). Wheel notches stayScroll.Modifier.scrollablehandles both. Custom handlers that only listen forPointerEventType.Scrollmust also handle Pan, or the app can set-Dnucleus.tao.trackpadPanEvents=falseto get AWT-styleScrollfor everything.Everything scroll-related enters the scene through
TaoSceneScrollRouter(window host and macOS NSPanel popups). Native views get the whole pan, begin and end included. The phase wire (RustSCROLL_GESTURE_*,popup_panel.m,TaoScrollGesturePhase) is guarded byTaoScrollWireDriftTest.Packaging
macOS app extensions (
.appex)#396, addresses #394. First-class embedding and signing of prebuilt
.appexbundles (e.g. a Network Extension) intoContents/PlugIns/. Nucleus does not build the.appex(that stays Xcode / Kotlin/Native); it copies, signs, and seals.macOS { entitlementsFile.set(file("packaging/app.entitlements")) appExtensions { extension("NetworkFilter") { appex(file("build/appex/NetworkFilter.appex")) entitlements(file("packaging/extension/NetworkExtension.entitlements")) // provisioningProfile(file("packaging/NetworkFilter.provisionprofile")) } } }Each extension is signed inside-out with its own entitlements and (optional) provisioning profile. The outer app is then sealed without
--deep, so the nested signature is preserved. Wired on the JVM/jpackage path, the DMG/PKG re-seal (electron-builder), and GraalVM native images. Unused, the new paths are no-ops.Follow-up from TestFlight validation: the jpackage path now copies with
cp -Rso the nested executable keeps+x(copyRecursivelydropped the POSIX mode and launchd could not spawn the provider, errno 111). The plugin warns when an extension is embedded into an app whose entitlements still grantallow-unsigned-executable-memory/disable-library-validation(both in the default entitlements); a host carrying them alongside a network extension has been reported not to launch.com.apple.security.cs.allow-jitis enough for the JVM.Caveats:
.app.runcannot exercise it; userunDistributable.com.apple.developer.networking.networkextension. Ad-hoc builds only prove bundling and signing.signing {}for a notarized GraalVM DMG.NEFilterManager/NETunnelProviderManager) is out of scope here.Demo:
examples/macos-appex-demo(minimalNEFilterDataProvidercompiled to a universal.appex).nucleusOptimizationandlastJdk#639. One-switch startup pack, with per-knob overrides. Does not enable AOT (
enableAotCacheis separate). Does not change the Gradle compile JDK.nucleus.application { nucleusOptimization = true nucleusOptimization { idleGc = false } }serialGcgarbageCollectoris unset. An explicit collector always wins.compactHeap-Xms32mand-XX:MaxRAMPercentage=25, unless already injvmArgs.singleJarjoinOutputJarswhen ProGuard is on) so the jpackage image contains a single JAR.idleGcbindNucleusContent/bindNucleusDialogContent, so decorated windows, dialogs, tabs and satellites all get it.lastJdk<gradle-user-home>/nucleus/jdklike the GraalVM toolchain. An explicitjavaHomealways wins.lastJdkis pinned to OpenJDK 27 GA (build 35, https://jdk.java.net/27/). Intel macs and Windows ARM fall back to BellSoft Liberica JDK 27 (Oracle dropped those ports). Liberica Lite is not used: Lite has nojmods, sojlinkcannot produce a runtime image (#669).App Store PKG is not notarized
#681, fixes #650. electron-builder was notarizing the macOS App Store PKG with the same credentials as the DMG. App Store packages must not be notarized; the plugin now emits
notarize: falsefor that job. DMG / ZIP are unchanged.jdk.jlinkdropped fromincludeAllModules#682, fixes #673. On a JEP 493 JDK (Temurin 24+ and other builds without
jmods/),jlinkrefuses to emit an image containingjdk.jlink.nativeDistributions.includeAllModules = truecopiedjava --list-modulesverbatim, which includes it.AbstractJLinkTasknow filtersjdk.jlinkout before--add-modules. A shipped app never needs jlink. The default module list never contained it, so default packaging is unchanged.Native library cache
#686, closes #303. The extraction path was hard-wired (
%LOCALAPPDATA%\nucleus\native,~/Library/Caches/nucleus/native,$XDG_CACHE_HOME/nucleus/native). An app that keeps config, cache, logs and natives under one directory can now relocate it. Candidates are tried in order; a directory that cannot be created or written to falls through:-Dnucleus.native.cacheDir=<dir>(NativeLibraryLoader.CACHE_DIR_PROPERTY). Works for libraries loaded before any application code runs, and bakes into the launcher.cfgthrough existingjvmArgs. The JVM does not expand${user.home}; a path computed at run time goes throughcacheDirectoryinstead. Do not point this at the install directory or inside a macOS.app(read-only for a standard user; writing inside the bundle breaks the signature).NativeLibraryLoader.cacheDirectory, set frommain()before the first native library loads.The root is resolved once, at the first extraction. A later assignment is ignored with a warning naming the root in use. A configured directory is validated with a real write probe (create + delete a temp file), not
Files.isWritable. The content-addressed layout of #304 (<root>/<platform>/<fingerprint>/<library>) is kept under the chosen root.Packaged applications built by the Gradle plugin ship their natives on
java.library.pathand extract nothing; this setting matters for fat JARs, IDE runs and distributions that bypass the plugin.A set-but-empty or relative
XDG_CACHE_HOME/LOCALAPPDATAused to make the cache root relative to the working directory. Those values are now ignored, as the XDG spec requires.Filesystem watcher
#683, fixes #570 and #571. No public API change.
One native watcher per
FsWatcher(#571). Every registration on that instance shares it: one inotify instance / FSEvents stream /ReadDirectoryChangesWloop, not one per path. Events are routed back by root inlib.rs. A root already watched is not re-watched (except non-recursive → recursive, done asunwatch+watch). The backend is created on the firstwatch()and dropped after the lastunwatch().The backend is handed the canonical root on macOS and Linux (one watch per real directory; inotify keys watches by inode and
notifykeeps one spelling per descriptor, so aliases must share it — Kotlin projects events back onto each registration's spelling). On Windows the registered spelling is used (canonicalize()yields\\?\paths there).macOS renames (#570). FSEvents reports an inode's accumulated flags: a rename of an old file arrives as
Create+Rename+Modifyon the gone path, whichnotify-debouncer-fullused to fold into a bareCreated(new); a delete of a pre-existing file surfaced asModified.FsEventsNormalizernow fronts the debounced backend and drops what cannot be true of the path right now. Renames the backend cannot pair areRemoved(old)+Created(new), never dropped.Rawdelivery never emitsMoved.Application lifecycle
#676, fixes #667.
nucleusApplication(andtaoApplication) takeexitProcessOnExit: Boolean = true, matching Compose Desktop'sapplication(exitProcessOnExit).After the last window closes, the JVM is terminated (
exitProcess(0)on a normal quit,exitProcess(1)after a fatal error). That is still required because Compose/Skiko initialisation touches AWT, whose non-daemon EDT would otherwise keep the process alive.exitProcessOnExit = falsereturns normally so the caller can continue in-process. A fatal error is then rethrown after the same SEVERE log.Tao follow-ups that are not on main
Resize present, macOS and Windows (#678, fixes #576). Animated
WindowState.sizeand title-bar double-click zoom trembled and trailed the window edge. Each resize, Compose presented one to two frames behind the new bounds and the compositor stretched the stale drawable over them.onResizedpresents a frame at the new size inside the resize dispatch (whatprepareFullscreenFramealready did for Decorated window title bar content freeze when enter full screen mode #327), without pumping the dispatcher there. Vendored tao'sset_maximized_asyncno longer uses the NSWindow animator: 16 ms steps eased overanimationResizeTime:, a new request cancels the chain in flight.nativeResizeanchors the stale drawable top-left instead of stretching. Idle zoom: 20 / 20 steps presented before the next arrived (was 2 / 20); median resize → present ~2 ms.Windows re-entrancy deadlocks (#646, ports #640 / #644). Three non-reentrant locks were held across a call that pumps or synchronously sends messages, so a nested window-procedure dispatch parked the event-loop thread in
WaitOnAddressfor good — the window went(Not Responding). Triggers: moving a window to another virtual desktop on Windows 11, Alt+Tab during startup, RDP disconnect, Explorer restarting (TaskbarCreated), end of an IME composition. tao 0.36.0 has the fixes; we vendor 0.35.0 and cannot bump (tao#1231 removed Windows subclassing, which ourPATCH(nucleus)set lives in), so they are hand-ported. Shipped on 2.5 asv2.5.13.GPU resource cache on macOS and Linux (#641). Windows already purges on
main. The Metal host and the Linux GL host now run the same policy. Sharedscene/GpuResourceCache.kt: the limit-toggle is the only primitive skiko gives us, and what actually reclaims is the purge (writing Skia's default 256 MiB budget was a no-op). macOS purges every 250 ms while sizes stream. Linux arms the purge inonResizedand performs it in the render pass, the one point where this host's private EGL context is current. No settle purge and noSystem.gc()nudge on either: macOS and GTK have no drag-end signal, so a timer would fire after every size change.NativeView / TextureView embed bugs (#659). The widgets already exist on
main. New headful monkeys (click storms, resize storms, 150-action random walks) found and fixed:NativeViewdisposed the platform view before detaching it (SIGSEGV);nativeViewHost()returned a new object per call.nativeSetContentOffsetcommitted GTK's toplevel out of band and aborted in cairo.MainEventsClearedfrom aWM_PAINTa modal loop never generates.NSTextField.mouseDown:runstrackMouse:untilMouseUp:on the Tao thread, so a synthetic press never returned;nucleusIOSurfaceTextureSourcenow retains theIOSurface.Dialog's disappearance animation.Window content stays on the UI applier (#649, port of #648, fixes #636). Window and dialog openers (including
Satellite,SatelliteWindow,Tab) are@ComposableOpenTarget(-1)with@UiComposablecontent lambdas, so a non-UI composable called in thenucleusApplicationscope — MapLibre Compose'srememberMapStatewas the reporter — cannot reclassify nested window content.COMPOSE_APPLIER_CALL_MISMATCHis escalated to an error in the two modules' test compilations; aComposableTargetIsolationFixturecompiles the reported shape.JNI exceptions are logged before they are cleared (#677, fixes #486). Every JNI upcall path used to
ExceptionCheck+ExceptionClear: a Kotlin listener that threw vanished, no JUL line, no stderr. Shared helpernucleus_jni_clear_exceptioninnative-common/nucleus_jni.hnow reports throughJniExceptionReporter(JULWARNINGwith the throwable — visible even whenallowNucleusRuntimeLoggingis off) and only then clears. All 34 native bridges go through it.NativeJniExceptionHygieneTestfails the build if a newExceptionClear/ExceptionDescribeappears outside the helper.launcher-linuxnever autolaunches the session bus. WithoutDBUS_SESSION_BUS_ADDRESS, GDBus spawneddbus-launch, which waited for an X display that never came. Nine of ninepreMergejobs that hit the 30-min cap were stuck in:launcher-linux:test.get_connectionnow treats "no address" as "no bus"; both native tests skip without one.GDK's
(0, 0, 1, 1)frame-extents placeholder is not published as a window's frame (tao patch 0007).TaoMonitor.isPrimaryno longer throws under GDK's Wayland backend, which names no primary at all.Windows redraws are served at the end of the batch, not inside it.
Headful screenshots are captured off the Tao thread (#658).
GraalVM native-test (#657): the test image is given the classes it was compiled against (Material 3 cases were dying with
NoClassDefFoundError).Demos
examples/satellite-demoexamples/tabs-demorememberSaveablestate following a tab between windowsexamples/jewel-tabs-demoTabStrip/TabData.Editorchromeexamples/tab-satellites-demoSatelliteWorkspaceper tab window, palettes drawing that window's selected tabexamples/reader-dock-demosideOrderputting the right side outside the bottom one, 1 dp + 5 dp-grip splitters, hover headers, Classic / Islands styles, RTL. Book tree and contents arefloatable = false+reorderable = false+dockSides = setOf(Right)— furniture, and no pane can be dropped in front of them. One dock per tab window hung onTabWindows(windowBodyWrapper), so the strip stays the top of the window and a tab change touches no panel.examples/macos-appex-demo.appexembedded and signed throughmacOS { appExtensions { } }scheduler-demoandservice-management-demomoved off AWT (java.desktop/sun.*add-opens gone; SMAppService completion handlers hop to the Tao main thread).Migration
From 2.5 / current
main:nucleus.decorated-window-tao. Dropbackend = NucleusBackend.….NucleusBackendandLocalNucleusBackendare gone.window.unsafe.awtWindow/awtDialogand Compose Desktop'sWindow/Dialog/TraywithnucleusWindow,HostedWindow/HostedDialog, and an AWT-free tray.androidx.compose.ui.window.v2todev.nucleusframework.window.tao.v2. Compose's own types are not accepted.@OptIn(ExperimentalNucleusApi::class)or-opt-in=dev.nucleusframework.window.ExperimentalNucleusApi. The surface may change without a deprecation cycle.TaoWindow.canPlaceOnScreen(orSatelliteScope.isCompositorPlaced), not on the compositor name. UsefloatingCaptionfor the strip the title bar leaves to the compositor's move.-Dnucleus.text.lcd=falseor-Pnucleus.text.lcd.patch=false.nucleusOptimization = trueis opt-in.lastJdkdownloads OpenJDK 27 (Liberica on Intel mac and Windows ARM) the first time a packaging orruntask needs it.minimizable/maximizabledefault totrue. ThemedNucleusWindowHostimplementations must take the new parameters. Dialogs and satellite palettes already pass the right values.exitProcessOnExitdefaults totrue. Passfalseonly if you need to continue in-process after the last window.-Dnucleus.native.cacheDir=orNativeLibraryLoader.cacheDirectorybefore the first native load. Packaged plugin builds skip extraction.PointerEventType.Scrollhandlers on macOS must also handle Pan, or set-Dnucleus.tao.trackpadPanEvents=false.Merged PRs
PRs that landed on
nucleus-2.6and are not already onmain:.appex)nucleusOptimizationstartup packdockSides/floatable/reorderable, in-hand tab reorder, one drop previewlastJdkLiberica fallback for Windows ARMexitProcessOnExitonnucleusApplication(#667)minimizableflag to drop the minimize button (#504)jdk.jlinkfromincludeAllModulesruntime images (#673)maximizableflag, off by default for satellite windows