Repository navigation
Nucleus 3.0 - #776
Draft
kdroidFilter wants to merge 796 commits into
Draft
Nucleus 3.0#776kdroidFilter wants to merge 796 commits into
kdroidFilter wants to merge 796 commits into
Conversation
Closed
|
Huge update 🚚 🎈 , Can't wait to try it. Thanks for the hard work 🙏🏼 |
Collaborator
Author
|
@janbarari You can already give it a try today; I’m publishing pre-releases on MC. Unfortunately, I had to increase latency during resizing on Windows, and I’d like to know if you can notice it on your end. |
Used |
|
Thanks again for the great work. Nucleus feels very confident to use on projects. |
`#595 NSTextInputClient answers and empty corporate commit` timed out now and then on hosted macOS runners at "empty setMarkedText unmarks" (1 in 15 and 1 in 40 iterations on a loop). Traced, every layer did its job: tao cancelled the preedit, TaoImeSession received the empty preedit with the session active and the field composing "xyz", and issued the edit. The edit was dropped by BasicTextField(TextFieldValue) itself, which calls onValueChange only for a result that differs from the value it was last composed with. The case cancelled as soon as onValueChange reported "helloxyz", before the field recomposed with it; cancelling then yields "hello", equal to the stale value, so no onValueChange, and the next recomposition restored the preedit. Wait for the field to be composed with the second preedit before cancelling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tab mouse flick from one strip to another merges the tab` failed once on Windows (PR #810's run, its first failure in 52 Windows runs) with only "timed out waiting for: the flicked tab merged into the first window": no way to tell a flick released short of the home strip from one resolved wrongly at the release. Both waits now report the aim, the home strip's rect, where Beta is, every group with its tabs and window bounds, the live drag and preview, the robot's aim, and every pointer sample the drag took with what it previewed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(tao): fix the macOS held-key repeat and #595 empty-setMarkedText headful cases
test(tao): report where a tab flick ended when the merge times out
fix(tao/linux): keep touchscreen crossing out of mouse input
fix(tao/linux): drain pending GTK events between loop steps
fix(tao/linux): preserve touchscreen sample timestamps
…-snapshots Fix hot reload crashes from rebuilt included-build dependencies
On a Wayland session the Linux backend binds shortcuts through org.freedesktop.portal.GlobalShortcuts, and every test that registers one pops the desktop's "add keyboard shortcuts" dialog (labelled "kover-linux" by NativeLinuxHotKeyBridgeTest) while ./gradlew preMerge runs locally. portalRegistrationAllowed gates the three registering tests: false only on Linux + Wayland (the same rule as detectBackend in nucleus_global_hotkey_linux.c) with CI unset. X11, macOS and Windows keep running them everywhere; CI=1 ./gradlew :global-hotkey:test runs them on a Wayland desktop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The static analysis registers every native library of the classpath as a resource, so libskiko was embedded in the image on top of the copy the packaging puts in lib/ (bin/ on Windows), which Skiko loads first from java.home. The JAR the image is compiled from now leaves out Skiko's library, every architecture of it, wherever that copy is made (macOS always, Linux/Windows unless headless). Fixes #821
…native-image Compile GraalVM images without the Skiko library shipped beside them
test(global-hotkey): register through the Wayland portal on CI only
A loop frame recorded before a resize could reach the screen after the same-turn present at the new size, and the presentedInDispatch skip then swallowed the next frame: static content stayed drawn at the old size.
…-stale-frame fix(tao): repaint a macOS frame the window was resized under (#817)
nativeGetClientSize read GetClientRect before show(), while the hidden window was not maximized yet, so the first frame of an initially maximized system-framed window still rendered at the restored size (800x600 composed, then 2560x1009 after the first WM_SIZE). nativeGetMaximizedClientSize computes it instead: the work area of the window's monitor minus the caption, the same formula as estimateRestoredClientSize. A maximized frame overhangs the work area by exactly its sizing border, so only the caption comes off.
Conflicts with #889 (#532), which moved the size request of every non-resizable Linux window onto its content box (tao patch 0017, util::set_content_size_request). That covers the system-decorated case this branch handled on its own, so the branch's content_size_request special case in window.rs and event_loop.rs is dropped in favour of it. The system-decorations tao patch becomes 0018, regenerated against the new base without the size-request hunks; README, CLAUDE.md and docs/internals/system-decorations.md follow the renumbering.
… behind it Measured on Windows 11 26100: IAsyncOperation.Cancel() closes the Hello dialog 2-7 s later at best and, in a run of attempts, not at all, the dialog staying up even after the app exits. It lives in CredentialUIBroker, which UIPI shields (WM_CLOSE, SendInput, UI Automation and TerminateProcess are refused), and Windows shows one Hello request at a time, so later requests queued behind it. A new Hello request now waits, up to 15 s after this process last cancelled one, for no Credential Dialog Xaml Host window to be visible; a caller cancelled meanwhile ends without showing anything. The caller still resumes at once on cancel. KDoc, README and docs/internals/authentication.md say the dialog may stay until the user dismisses it, as Chrome's reauth and other libraries do.
…ation # Conflicts: # .github/workflows/build-natives.yaml # .github/workflows/publish-maven.yaml # CLAUDE.md # README.md # THIRD_PARTY_NOTICES.md # settings.gradle.kts
feat: Window / DialogWindow with the platform's own decorations (#841)
The unix-process subject carried our own pid, which a pid namespace makes meaningless: in a Flatpak the JVM is pid 2, polkitd looked up the host's pid 2 (kthreadd) and answered "process with PID 2 has been replaced", while availability() reported available. The subject is now system-bus-name with the unique name Hello returned: polkitd resolves the owner through the bus daemon, which reads the pid from the socket credentials in the host's namespace. Same process on the host. Verified on GNOME 50 / polkit 127 in a flatpak build sandbox (Platform 25.08): no system-bus access -> Unavailable, no hang; --system-talk-name=org.freedesktop.PolicyKit1 or --socket=system-bus -> the host agent prompts and the cancel dismisses it. Host smoke tests unchanged.
feat(authentication): Kotlin Multiplatform device-owner authentication
Windows backend over Windows.Services.Store, forked from tauri-plugin-iap and decoupled from Tauri: products, purchase, consumables, subscriptions, license change updates. macOS is not supported yet and Linux has no store; both answer Unsupported from Kotlin, with no native library.
InAppPurchase.appLicense() reads StoreAppLicense (active, trial, time left, trial unique id, SKU), purchaseApp() buys the app itself to convert a trial, and appLicenseUpdates reports license state changes from OfflineLicensesChanged. An install the Store did not license reads as an active non-trial license without a SKU, so isFullLicense also requires one.
… Microsoft Store system.purchases drives in-app-purchase on Windows: app license and trial, full-version purchase, add-ons, consumables, purchases and the updates the Store sends unasked. The Lab's AppX identity moves to its own reservation, KDroidFilter.NucleusLab, which the probe is verified against.
javaaccessbridge.dll imports jawt.dll, which Windows resolves from the executable's directory, but the plugin only copied it to bin/ for Skiko. With Java Access Bridge enabled (screen readers, jabswitch -enable) AWT failed at startup with UnsatisfiedLinkError: javaaccessbridge, unless a JDK on PATH happened to provide a jawt.dll of its own. Seen on the Nucleus Lab installed from the Microsoft Store.
Verified on the Nucleus Lab installed from the Microsoft Store (7-day trial): TrialTimeRemaining reports the unlimited sentinel for a time-limited trial, so trialTimeRemaining read null and the trial looked unlimited. It is now derived from ExpirationDate whenever there is one. The same install shows TrialUniqueId empty and IsTrialOwnedByThisUser false: both only describe usage-limited trials, which the KDoc now says. It also confirms a Store install carries a SkuStoreId (9MV8ZRKS4KT4/0011).
On the Nucleus Lab installed from the Microsoft Store, converting the trial to a full license never produced an app license update. The OfflineLicensesChanged subscription was made on the thread that first collected an update flow, the UI thread, an STA (tao's OleInitialize), where RoInitialize(MULTITHREADED) fails and the StoreContext stayed in that apartment. It is now made from a dedicated MTA thread, and a Rust test subscribes from an STA and checks the apartment. Every license notification is also logged at FINE, and the Lab probe lists those lines, so a real install shows whether notifications arrive.
StoreUpdates checks the Store for a newer version of the app (GetAppAndOptionalStorePackageUpdatesAsync: version and mandatory flag), installs it from inside the app with the Store's dialog or silently, streaming StorePackageUpdateStatus progress as a Flow, and opens the app's Store page. It shares the purchase bridge: progress travels as updateProgress events routed to the request that asked, and cancelling the collection cancels the download. New StoreErrorCode.UpdateBlocked for an update the Store holds until charging or Wi-Fi. The Lab gets a Store updates probe to verify it on the real listing.
On the Nucleus Lab installed from the Microsoft Store, the purchase dialog offered an owned durable add-on, and the owned app itself (full license, SKU 0010), for sale again, asking to pay a second time. Microsoft documents that a non-expiring durable can only be purchased once and recommends checking the license before offering a purchase; no other library checks first. purchase() (non-consumables and subscriptions) and purchaseApp() now read this device's licenses before any window and answer Success(alreadyOwned = true). A device whose licenses have not synced yet still gets the Store's dialog.
The first in-app update of the Nucleus Lab from the Microsoft Store (1.0.4 -> 1.0.5) installed, closed the app and left it closed. Microsoft documents the relaunch of a desktop app the Store updates as opt-in through RegisterApplicationRestart. StoreUpdates.install now registers the app right before installing, for updates only (no crash, hang or reboot restarts), and unregisters when the install returns without replacing the process. Windows skips it for an app that ran under 60 s. The KDoc no longer claims Windows relaunches the app on its own.
…et size
packageGraalvmAppX never received the appx { } logos (only the JVM task did), and its
fallback, the Windows .ico, cannot be read by ImageIO: nothing was staged and the package
shipped electron-builder's sample logos, which the Microsoft Store rejects as generic.
- the GraalVM pipeline wires the four logos and the Linux PNG fallback, as the JVM one
- the fallback reads .ico files (their largest PNG or 32/24 bit bitmap entry)
- each logo is generated at scale-100 to 400 and the app icon at targetsize-16 to 256,
plated and unplated, so electron-builder indexes them in resources.pri and Windows picks
the one for the display instead of stretching a single 44 px image
fix(plugin): the GraalVM AppX gets its logos, at every scale and target size
The module covers more than purchases now (app license, trial, Store updates) and only ever targets the Microsoft Store, so it is named after it: - module microsoft-store, artifact nucleus.microsoft-store - package dev.nucleusframework.store.microsoft, native lib nucleus_msstore - one root object: MicrosoftStore.purchases (list/restore replace purchases/restorePurchases) and MicrosoftStore.updates
…ndows feat(microsoft-store): Microsoft Store purchases, app license, trial and updates
Minimizing a window hides the windows it owns (satellites, dialogs) and restoring it shows them again through WM_SHOWWINDOW/SW_PARENTOPENING, without TaoWindow.show ever running. DWM has dropped the hidden window's frame and the scene is clean, so nothing was presented: the satellite stayed white until the pointer passed over it. The deco WndProc arms on SW_PARENTOPENING and, once the window is really mapped (WM_WINDOWPOSCHANGED with SWP_SHOWWINDOW), calls back into the scene host, which forces its next present. Calling back from WM_SHOWWINDOW itself is too early: the window is still hidden and DWM drops that present too.
…hite fix(tao): repaint satellites Win32 re-shows on their owner's restore
…nt on A DirectManipulation glide closed with MOMENTUM_ENDED, an immediate PanEnd. Compose flings at PanEnd with the velocity of the last steps, so fingers put down to stop a fast glide (INERTIA -> RUNNING) threw the content another half-glide. The glide now closes with a deferred ENDED: the PanEnd lands past Compose's velocity horizon, and fingers that move again continue the pan.
fix(tao): stopping a touchpad glide no longer flings the content on
Flathub repackages the GraalVM native image instead of building from source, the path a closed-source Nucleus app takes. Running the Lab from a local Flatpak repository turned up three run-time bugs every such app would hit: - ExecutableRuntime reported the archive's marker (TAR): /.flatpak-info now wins over it, so the app reports FLATPAK and isSandboxed() - the Wayland app_id was the executable's name: Tao sets the GTK program name to FLATPAK_ID before GTK starts - GNOME minimize/maximize vanished: GSettings only sees the app's keyfile in a sandbox, so button-layout is read and watched through the Settings portal there release-desktop now publishes the Linux tar.gz; the manifest, metainfo, screenshots and the local repository loop live in examples/lab/app/packaging/flathub and docs/internals/flathub.md.
feat(flathub): ship the Lab on Flathub from its released tar.gz
New `flatpak` module, the Flathub counterpart of MicrosoftStore.updates, over org.freedesktop.portal.Flatpak (Rust JNI bridge on zbus): - `status`: the portal's update monitor, as Available / Installed / UpToDate. The portal has no "check now" (it polls every 30 min), so this is a Flow, not a check() - `install`: UpdateMonitor.Update with progress; the first one shows the portal's "Update <app>?" dialog over the parent window - `restart`: hands the single-instance lock over and Spawns the latest commit, then exits - `openStorePage`: appstream://<app id> through the OpenURI portal Spawn is always given fds 0-2: without any, the portal hands the environment to `flatpak run` on fd 0, which it rejects, and nothing starts after the app already quit. The Lab's system.flatpak-updates probe drives it, deep-link flags included (?watch&install&restart). Verified end to end from a local Flatpak repository: detection, the dialog, install, restart after an in-app and after a background update.
feat(flatpak): in-app updates through the Flatpak portal
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 3.0 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. Cmd+Q and Dock → Quit go through each window'sonCloseRequest. Native OS callbacks land on the host UI thread, not the AWT EDT. A stalled Windows event loop is reported instead of freezing silently. Recognized trackpad pinch reaches Compose asScaleevents.nucleusApplicationinitializes FileKit and skips single instance in dev runs. Packaging provisions its own Node.js, ships Nucleus JNI libraries next to the app instead of extracting them, takes ANGLE from Maven (D3D11 only), and gains macOS.appexembedding, a Developer ID PKG with install scripts, a startup optimization pack,lastJdk, JRE fonts stripped from the runtime image, G1 in native images on Windows / macOS, jlink / App Store PKG / NSIS fixes, and SemVer pre-release versions on Windows and macOS. Native libraries can extract to an app-chosen cache.FsWatchershares one native watcher per instance and delivers macOS renames asMoved. Windows NSIS installs update without closing the app, and updates can be tested without publishing a release, and a manifest file name can no longer escape the staging directory. A new Kotlin Multiplatformsharemodule opens the system share sheet. Tabs can live in app-owned windows and cross between linked workspaces. Mouse side buttons reach Compose, and a macOS Control-click is a secondary click. FileKit dialogs attach to a Nucleus window. Kotlin 2.4.20 and Compose 1.12.1 are required.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.controlButtonsDirection(#733) puts a floating satellite's controls on the main window's side. An RTL app on an LTR system that passesControlButtonsDirection.SystemNativeto its main window had its satellites' traffic lights on the opposite side. DefaultAuto, unchanged.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.minExtent/maxExtent(Satellite(...), defaultMinDockExtent..∞, #695) bound a panel's docked thickness: on its own layer, on a shared split side (thickest minimum, thinnest maximum, minimum wins), on the splitters, ondock()/restore(), and in the drop preview. The floating window is not constrained.restore()applies placements before extents.dock()and the preview share one width (dockSeedExtent) and one weight (dockSeedWeight), so what lights up is what the release produces — a split side's landing rect is the stack at the thickness the side takes once the panel joins it.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.Direction.
reorderTargetandinsertionIndexare direction-aware (a right-to-left strip used to resolve every drop mirrored). Both take the direction the strip publishes onHostGeometry.layoutDirectionand infer it from the slot order only without one: a single tab cannot tell, and a right-to-left single-tab strip resolved left to right flipped the index on every sample — two cards sliding about (#695). The drag ghost is laid out in the source strip's / dock's direction.Customizable previews (#695):
TabEntry.thumbnailis app-assignable, so a restored layout can put its saved picture back (assign before the tab is shown; the recorder's capture replaces it once it is).TabPreview(tab, modifier)is the one composable for a tab's thumbnail; the stock hover card is built on it.TabWindows(dragGhost)draws the card under the pointer,TabStrip(dropGhostCard)the card in the landing slot. Both default to the now-publicTabGhostCard(tab, modifier).TabDropGhost.tabcarries the entry.TabStrip(tabLeading, tabTrailing)are per-tab slots; the 6 dp gap is charged only when a slot draws something, so the stock chip is unchanged.WorkspaceDragKind(Window/Transfer) replacesSatelliteDragKindand is shared bySatelliteWorkspace.dragKindandTabWorkspace.dragKind. It is observable and reportsTransferonly once the platform session exists.TabWindowsandDragGhostWindoware pinned to@ComposableOpenTarget(-1): the inferred target flipped with incremental compilation (Target-inferred nucleusApplication scope lets a callee's non-UI composable target flood the window scope #636).Title bar slot (#731).
TabWindows(titleBar = { strip -> … })keeps an app's own title bar (a gradient,newFullscreenControls,ControlButtonsDirection.SystemNative). The default is the now-publicDefaultTabTitleBar, the previousBasicTitleBar(FillCenter) { strip }. Innucleus-applicationthe slot has aNucleusDecoratedWindowScopereceiver;rememberNucleusScope()is extracted frombindNucleusContentso the title bar gets the Nucleus scope without running the window binding a third time.App-owned tab windows (#732).
TabWindowsis now built from public parts, so an app can open its own window per group (state, title, key handling, close policy) and draw its own strip with the exact workspace behavior:BindTabGroupWindow(workspace, group, window)attaches the window to its group and detaches it on dispose (closesOnQuit = false).TabGroupWindowPlacement(group, state)applies the group'splacementRevision(restore, tear-off) to an app-ownedWindowState, never fighting a user drag.ApplicationScope.TabDragGhostWindow(workspace, compositionLocalContext, content), plusNucleusApplicationScope.TabDragGhostWindow(workspace, nativeContextMenu, content).TabStripDrag+TabStripScope.rememberTabStripDrag(reorderAnimation): carry along the strip, slide home, hand-over to another window.Modifier.tabStripGrip(scope, drag, tab, index, cursor): slot, placement,noWindowDragand the drag handle in one place.Modifier.tabStripCarry(drag, tab)is the carried offset.TabGripCursorontabStripGripandTabStrip(gripCursor):Defaultkeeps the open/closed hand,Nonekeeps the strip's own pointer.Linked workspaces (#734). An app with one
TabWorkspaceper virtual desktop lets a tab cross between them.TabWorkspace.linkedWorkspaceslists the other workspaces whose strips accept this one's drags (tried after its own, pointer drags only).onForeignDrop(tab, into, target)is called on release over a linked strip; the workspace leaves the tab in place, because only the app can declare it in the other workspace. Withnull(default), linked strips are no target. Where strips of two workspaces overlap, the drop follows the in-workspace rule (a strip under the pointer beats one the card only reaches, then the most recently focused window) on one process-wide focus clock.Fixes: a tear-off after
restore()never takes a restored group's id (the counter restarted with the process, so two windows could share an id, or pending tabs landed in the torn-off window) (#731).tabSlotignores a tab that just left its group, which crashed the strip (found by a 400-operation random walk) (#732).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.Trackpad
Scroll and pan (macOS)
#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.Pinch as Compose
Scaleevents#662, fixes #660. Platform-recognized pinch (AppKit magnify, GDK pinch, Windows / Linux Ctrl+wheel) used to be replayed as two synthetic
PointerType.Touchcontacts 120 px either side of the cursor: Compose re-recognized an already-recognized gesture (span / slop delay, ~13 % of pinch lost before any zoom), and a contact could land in neighbouring chrome (the MapLibre edge interruptions). It is nowScaleStart/ScaleChange/ScaleEndat the cursor,scaleGestureFactorbeing the per-event ratio.Modifier.transformableand MapLibre consume it; a +1 % pinch zooms immediately.detectTransformGesturesdoes not see Scale events (Compose 1.12). Apps pinching through it needModifier.transformableor aPointerEventType.Scale*listener.TaoTrackpadRotationContactsids, whichTitleBarignores (a rotation over the bar started a compositor move); an interrupted rotation cancels pointer input before releasing the contacts, so it is no longer a tap.Mouse buttons
Back and forward (#728). Side buttons reach Compose as
PointerButton.Back/PointerButton.Forwardon all three OSes. Windows and Linux used to send every extra button as the same code, macOS reported them all as Middle.TaoMouseButtongainsBACK = 3/FORWARD = 4andOTHERmoves from3to5(aconst val: recompile code that reads it). LinuxNativeViewcapture boxes map GDK buttons 8/9 instead of passing them as a primary click.TaoMouseButtonWireDriftTestguards the Rust / Kotlin codes. Native popup panels and the WindowsNativeViewblending overlay still drop side buttons.Control-click on macOS (#737, fixes #736). A left press with Control held is the secondary button, in the window (
TaoComposeSceneHost.onPointerButton) and in native popup panels (popup_panel.m). The choice is latched at the press, so the release names the same button even if Control is let go first.ContextMenuArea, text-field menus andisSecondaryPressednow see it.Packaging
Node.js is provisioned, not required
#709 then #711. Every format except
RawAppImagegoes through electron-builder, which needed anode/npmonPATH. The plugin now downloads Node.js fromnodejs.org(verified against the release'sSHASUMS256.txt) into<gradle-user-home>/nucleus/nodejs, like the GraalVM and packaging JDK toolchains; the three shareToolchainDownloads, and parallel format tasks in one daemon no longer race on the install lock (#711).nativeDistributions { nodejs { version = "22" // default; a major line, "lts", or a pinned release } }Precedence:
compose.electronBuilder.nodePath, thenNUCLEUS_NODE_HOME, then the provisioned install, thenPATH(also the fallback when the download fails). A floating line is sticky once downloaded. electron-builder moves to 26.16.1. CI dropsactions/setup-nodeeverywhere;universal-macosprovisions electron-builder the same way throughprovision-electron-builder.shinstead ofnpx --yes.Nucleus JNI libraries ship next to the app
#712. Packaged apps carried every Nucleus native library inside its module JAR and extracted them on the first launch after an install or update. Only Nucleus libraries move — each
nucleus.native-modulemodule ships aMETA-INF/nucleus/native-libraries/nucleus.<module>manifest, and every othernucleus/native/entry (an app's own library, a third party's) stays in its JAR.$APPDIRnext to Skiko, other platforms' copies are dropped, and the launcher gets-Dnucleus.native.libraryPath=$APPDIR, whichNativeLibraryLoadertries first. Only done whencore-runtime'sbundled-native-librariesmarker is on the classpath (an older loader would not look there). The sandboxed pipeline keeps its layout.Contents/MacOSon macOS, stripped / patched / signed with the other dylibs).Measured on Windows (
tao-demo, 9 DLLs): ~40–90 ms of extraction on the first launch after an update, ~10 ms per warm launch.ANGLE from Maven, D3D11 only
#707. The Windows ANGLE DLLs were extracted at build time from a pinned Electron release: 292 MB of download for 8.5 MB of DLL, carrying Vulkan, WGL, D3D9, SwiftShader and GLES1 backends Tao cannot reach. They now come from
dev.nucleusframework:nucleus.angle-natives(8037.1, the Chromium branch), built from unmodified upstream sources of the branch stable Chrome ships with everything but D3D11 disabled.libGLESv2.dllx64 is 30 % smaller, arm64 is new, andgdi32.dllleaves the import table. A fresh worktree no longer lacks the DLLs. Dependabot is configured for Gradle and GitHub Actions, with ANGLE in a group of its own.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. Also honored in GraalVM native images, throughnucleus-app.properties(#715).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).Developer ID PKG with install scripts
#688, closes #249. PKG was hardwired to the Mac App Store, so a PKG for Jamf / MDM or manual install was not buildable.
macOS { pkg { appStore } }now picks the channel (defaulttrue, existing builds unchanged):macOS { pkg { appStore = false preInstall.set(file("packaging/macos/preinstall")) postInstall.set(file("packaging/macos/postinstall")) } }appStore = falsetakes the DMG's non-sandboxed pipeline. electron-builder signs the installer with the matching Developer ID Installer certificate, handed the bareNAME (TEAMID)fromsigning {}(it prepends the certificate type and rejects a prefixed qualifier), a DSL keychain travels asCSC_KEYCHAIN, andnotarizePkgnotarizes the.pkg. A missing certificate would produce a silently unsigned package, so the task checks the result withpkgutil --check-signature.preInstall/postInstallare staged forpkgbuild --scripts(shebang required). The App Store rejects install scripts (error 90254), so they requireappStore = false, enforced at configuration time.PackageInfo, and Installer ran it twice on a real install. The stagedpreinstall/postinstallare shims that skip the per-bundle pass and exec the app's copy.ExecutableRuntime.isSandboxed()(APP_SANDBOX_CONTAINER_ID). The scheduler gates on it instead ofisPkg(), and a Developer ID PKG is self-updatable while the store build stays excluded. Build-side, the channel ismacOS { pkg { appStore } }itself: nothing public replacesisStoreFormat, a build script readsappStore.macOS.appStoreandTargetFormat.isStoreFormatare deprecated at ERROR level rather than re-aliased: the former was ignored and defaulted tofalse, so an alias would have silently flipped existing builds to the other channel.JRE fonts stripped from the runtime image
#694, port of JetBrains/compose-multiplatform#5706.
createRuntimeImagepasses--exclude-files=glob:/java.desktop/lib/fonts/**. Compose draws with its own fonts; on JetBrains Runtime 21 the image drops from 87 MB to 78 MB. Temurin and Liberica ship nolib/fonts, so nothing changes there.runand GraalVM images are unaffected. An app that renders text through AWT / Swing opts out:nativeDistributions { stripJreFonts = false }GraalVM 25i4
#698. The default innovation toolchain moves from
25i3(GraalVM 25.3.4.1) to25i4(GraalVM 25.4.4.1.1, JDK 25.0.4.1.1). Roll back withgraalvm { toolchain { version = "25i3" } }.G1 in native images on Windows and macOS
#708.
NativeImageGarbageCollector.G1was gated to Linux. Oracle GraalVM ships the G1 libraries for Windows and macOS since 25.4, so the gate is now a version floor read fromGRAALVM_VERSIONin the toolchain'sreleasefile. 25.3 advertises--gc=G1but dies at link time (g1gc-cr.lib); an unreadable version off Linux falls back to Serial. G1 stays Oracle-only.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.NSIS
#718.
deleteAppDataOnUninstallnow removes%APPDATA%\<appId>in both pipelines (a custom GraalVMimageNameleft it behind).package.jsonis regenerated on every packaging, so apackageNamechange reaches the installer name. Repackaging on Windows no longer fails withCannot delete … after 5 attempts: the read-only flag of jpackage's launcher is cleared before deleting.app-image.Classpath order in packaged launchers
#729. jpackage builds
app.classpathfrom its--inputdirectory sorted by name, so when two JARs define the same classes a packaged app loaded whichever sorted first, while./gradlew runfollows Gradle's order. Every Jewel app was affected: the IntelliJ icon libraries pullkotlinx-coroutines-core-jvm-1.10.2-intellij-2, which sorted before1.11.0and failed withNoSuchMethodError: Job.cancel$default.LauncherClasspathOrderrewrites the launcher.cfgclasspath in Gradle's order (main JAR first) right after jpackage, before macOS signing and the Linux pathing-jar collapse. The sandboxed strip task records.classpath-orderfor it.AWT fonts in macOS GraalVM bundles
#735. Any
java.awt.Fontuse crashed in macOS native bundles (Problem reading font data/Fontconfig head is null); Jewel hits it at startup.sun.awt.fontconfigpointed at the binaryfontconfig.bfc, which the JDK always parses as text properties, andresolveMacOsFontConfiglooked in the wrong directory whenexecDirended in/.. The plugin now ships the JDK'sfontconfig.properties.srcasContents/Resources/fontconfig.properties, the path is normalized, and theFont.createFont(int, InputStream)substitution applies on macOS too.Pre-release versions on Windows and macOS
#255. jpackage on Windows and macOS rejects SemVer pre-release / build metadata (
2.3.5-beta.7failed withinvalid component [5-beta.7]), and every installer format is built from its app image. The plugin now strips the suffix wherever the platform needs a numeric version; no new DSL.--app-version(Windows / macOS) gets theMAJOR.MINOR.PATCHcore. Linux keeps the full version.CFBundleVersionfalls back to the numeric version whenpackageBuildVersionis unset. GraalVM bundles do the same inInfo.plist; the Windows.rcbuildsFILEVERSION/PRODUCTVERSIONfrom the numeric version (2.3.5-beta.7used to become2,3,0,7) and keeps the full one in the strings.packageVersionFor(format → OS →packageVersion→project.version→1.0.0). The GraalVM Windows.rcused to skipproject.version.nucleus-app.propertiesuses the same lookup without the1.0.0default, soNucleusApp.versionand the updater see the full pre-release even thoughjpackage.app-versionis shortened.Auto-update
Hot update on Windows
#720. On Windows,
installAndRestartclosed the app, ran the NSIS installer and relaunched it: ~13–15 s with nothing on screen. The app now stays visible for the whole update.App.exe+app\App.cfgstay at the install root, the runtime and the app move toversions\<version>\, so a new version installs next to the running one.NUCLEUS_HOT_UPDATE=1, which makes the generated NSIS hooks neither kill the app nor delete its files.UpdateHandoff.signalReady()). The new version deletes the retired one.pendingRestartVersionand switch withrestartToInstalledVersion(relaunchArguments).consumeUpdateEvent/wasJustUpdatedno longer report an update whose install failed.New public API:
UpdateHandoff,VersionedInstall,SingleInstanceManager.releaseForHandoff(),installAndRestart(file, relaunchArguments),pendingRestartVersion,restartToInstalledVersion(). GraalVM native images, MSI and portable builds keep the classic update.Test updates without publishing a release
#725. Modeled on electron-updater's
dev-app-update.ymland Squirrel / Velopack local sources../gradlew run -Pnucleus.updater.simulate=update-Pnucleus.updater.feedUrl=build/compose/binaries/main/nsisserveUpdateFeedUpdateSimulation(in code ornucleus.updater.simulate*) plays available / up-to-date / check-error / download-error / checksum-error, timed progress, differential and "just updated".UpdaterConfig.allowLaunchOverrides. An unpackaged run always honours them and never installs.serveUpdateFeedpackages, then serves the manifests and artifacts with byte ranges on127.0.0.1:8421.updater-testing:UpdateFeedServerwith fault injection (FeedFault.Status / Delay / Throttle / Truncate / Corrupt / IgnoreRange).latest*.yml(electron-builder wrote none without apublishprovider); only artifacts of the packaged version are listed, and old manifests are deleted first (clients downloaded the previous installer or failed its checksum);GenericProvideracceptshttp://[::1].Manifest file names
#740. The artifact's file name comes from the manifest's
urlfield (remote input) and names the staged download, so../victim.zipresolved outside the staging directory and the download + rename could overwrite any file the app can write.downloadUpdate()now throwsParseExceptionfor a name that is not a single path component (empty,.,..,/or\) before creating the staging directory or downloading anything. No legitimate manifest is affected.Share sheet
#727. New
sharemodule (dev.nucleusframework:nucleus.share), the repo's first Kotlin Multiplatform library, inspired byrobius-share. Rust only on desktop; Android, iOS and the web are pure Kotlin.ACTION_SEND/ACTION_SEND_MULTIPLE)UIActivityViewControllerNSSharingServicePicker(Rust, JNI)DataTransferManagerdesktop interop (Rust, JNI)xdg-openfallback (Rust, JNI)js,wasmJs)ShareSheet.shareissuspendand safe from any dispatcher. It reports neither the picked target nor a cancel. Errors areShareException(ShareError), validated in common code first.NucleusWindow.share(nucleus-application,compileOnlyonnucleus.share) resolves the parent: HWND,NSWindow(picker pointing atshareAnchor(density)or the top centre),x11:<xid>, or a Waylandxdg_foreignexport released once the portal dialog closes.NucleusShareProvider(${applicationId}.nucleus.share, no AndroidX). The web needs a secure context and a user gesture, and sharesblob:/data:/ same-origin URIs.nucleus.native-modulewiresjvmProcessResourcesfor KMP modules; the root build wires per-source-set detekt tasks intocheck.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 next to the app and extract nothing (#712); 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.On Linux the closed window no longer stays mapped and frozen after
nucleusApplicationreturns: the GDK and secondary X11 displays are flushed once the loop ends (#714).System quit is cancellable (#696). Cmd+Q and Dock → Quit on macOS used to terminate immediately, bypassing the windows' close handlers. They now deliver
onCloseRequestto every open Tao window, so the same save / confirm / cancel handling works on all three OSes. With no window open, the event loop exits. No new public API.FileKit is initialized for you (#718). When FileKit is on the runtime classpath and the app has not initialized it,
nucleusApplicationcallsFileKit.init(NucleusApp.appId), so dev runs, packaged JVM apps and native images share one data directory. FileKit iscompileOnly; without it the call is a caughtLinkageError, no reflection. Opt out withinitializeFileKit = false.FileKit dialogs attached to a window (#722).
NucleusWindow.withFileKitDialogSettings { settings -> … }fillsFileKitDialogSettings.parentfrom the window: HWND on Windows,x11:<xid>on X11, anxdg_foreignexport on Wayland (closed when the block returns or throws). macOS needs none: FileKit's panels are app-modal.filekit-dialogsiscompileOnly. FileKit moves to 0.16.0.Single instance is off in dev runs (#717).
enableSingleInstancedefaults to!ExecutableRuntime.isDev(), so a second debug instance, or one started while a packaged copy runs, is no longer forwarded and exited. Packaged builds are unchanged.No system-theme polling (#716). Compose 1.12 polls the OS theme every second on
Dispatchers.IO(ComposeUiFlags.pollSystemTheme). Nucleus already providesLocalSystemThemefrom its reactive detector, sonucleusApplicationclears the flag.Native callbacks on the UI thread
#687.
notification-linux,notification-windows,media-control,launcher-linuxandlauncher-macosposted their native callbacks withSwingUtilities.invokeLater. Under Tao the AWT EDT is not Compose's UI thread, and calling it initialises AWT in a process that may want none (graalvm { headless = true }).NucleusUiThread(new,core-runtime) is the one marshalling point: an executor the backend registers, falling back toEventQueue.invokeLaterwhen no Nucleus entry point ran.postalways queues, never runs inline. The OS modules stay Compose-free;javax.swingis gone from all five.TaoApplication.run, so a baretaoApplicationhost is covered too. The same place recordsWindowBackend.Tao, which a bareTaoApplication.runapp used to report asAwt.global-hotkey(#721) invokedHotKeyListeneron the native thread that received the press. Its three bridges now post toNucleusUiThreadand resolve the listener inside the posted block, so a press still queued whenunregister()returns is dropped.Verified on Linux against real D-Bus (notification close, MPRIS play, dbusmenu event): callbacks run on the Tao
mainthread. Without a Nucleus entry point they land on the same threadSwingUtilities.invokeLaterwould use, GUI or headless.Event-loop watchdog
#710, closes #643. A deadlocked event loop produces no exception — to the JVM the thread is a healthy
RUNNABLEin native code — so #640 froze the app in complete silence.TaoEventLoopWatchdogpollsIsHungAppWindowevery 2 s and, once a window has been hung past a grace period on top of Windows' own ~5 s, logsSEVEREwith a full thread dump (event-loop thread first). The probe is a pure OS query: it sends nothing to the loop, unlike aWM_NULLprobe, whose inline sent message is exactly the re-entrancy that deadlocked #640.The shape is Electron's: the framework logs and raises callbacks, the "wait or quit" prompt is the app's to build.
nucleusApplication(args) { onUnresponsive { showWaitOrQuit() } onResponsive { hideWaitOrQuit() } expectUnresponsive { importHugeProjectSynchronously() } // not reported }nucleus-tao-watchdog-eventsthread — not the stuck UI thread, and not the sampling thread, so a blocking listener never delays detection.-Dnucleus.tao.watchdog=falsedisables it (=trueforces it on under a debugger),-Dnucleus.tao.watchdogGraceMs=<ms>retunes it,-Dnucleus.tao.watchdogDialog=truealso pops the native dialog._NET_WM_PINGperturbs the loop it observes.Dependencies
#723. Kotlin 2.4.20, Compose 1.12.1, Jewel 0.41.0, Ktor 3.6.0, Coil 3.6.3, materialkolor 5.0.1, composenativetray 2.1.6, composewebview 1.0.3, BCV 0.18.2, GraalVM native build tools 1.1.14. Compose 1.12.1 types
LocalSystemThemeas its ownSystemTheme;ProvideNucleusSystemThemeprovides that enum, soisSystemInDarkTheme()still reads Nucleus's reactive detector.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.Edge double-click zoom, macOS (#693, follow-up to #678). AppKit's own animated frame changes — edge double-click (
_zoomToScreenEdge:), Window-menu tiling,zoom:— run a private run-loop mode tao's observers never see, so the content replayed the whole animation in one burst at the end. The vendoredTaoWindowoverridessetFrame:display:animate:and routes every animated frame change through the #678 stepper, now the single engine (fullscreen transitions untouched).Resizedevents dispatched with the native frame elsewhere: 42 → 0.Linux / Wayland resize (#689 then #691, closes #444).
wl_egl_window_resizeonly records a pending size, so painting at the requested size put ~1 frame in 3 that many rows off. The paint size now comes fromeglQuerySurface, not a prediction — and no desktop-environment check is left in that decision. On NVIDIA the driver reallocates mid-frame, so frames that pushed a resize first bind and clear the default framebuffer, then query (12 → 0 mid-frame reallocations). The scene keeps the window's size; only the render target follows the buffer, so layout no longer lags. Wrong-size frames on a maximize/restore storm: 16/64 → 0.drawhook on the GtkWindow (nativeConnectToplevelDraw, size fromgtk_window_get_size) with the content sub-surface inset_syncand swap interval 0, and waits for the swap before returning. Mesa adopts awl_egl_windowresize only while no back buffer is acquired, so the resize is pushed beforeeglMakeCurrent. Frames with the right edge moving on a scripted left-edge drag: 82 → 0 (a plain GTK3 window: 0). In a native image the draw hook never connected (method looked up on an unregistered anonymous class) and was retried every frame; it now resolves onToplevelDrawCallbackand is tried once per window (#704).queue_drawunanswered for 50 ms falls back to the event-loop render path, it is never armed while maximized / tiled / fullscreen, and the content's opaque region leaves its bottom row out so Mutter never culls the toplevel (which froze GDK's frame clock and with it pointer motion).Input-source indicator, macOS (#690). The badge a Caps Lock bound to layout switching raises is positioned from
firstRectForCharacterRange:. TaoView answered a process-global caret rect nothing invalidated, so the badge kept appearing where a destroyed field used to be — or, before any field was focused, in the bottom-left corner of the screen. The rect is now scoped to the view that pushed it (NSZeroRectotherwise, the only shape AppKit reads as "no insertion point"), theNSTextInputClientoverrides are installed per window creation, and the input context is deactivated when the session ends (guarded by its activation token).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.
No internal
onGloballyPositioned(#705, closes #560) indecorated-window-taoandnucleus-application. Geometry sites (NativeView,windowGlassRegion, caption hit zones,tabSlot,DockLayout,publishHostGeometry) use an internalModifier.onPositionChangedthat fires only on real moves while keeping the modifier's own coordinates; the drag sessions useonPlaced. PlainonLayoutRectChangedwas not a drop-in: it reports the node's outer rect, which differs once apadding/offsetprecedes the modifier.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). The live-theme e2e is skipped on runners without an XDG portal (#703).A window created maximized restores to its floating size, macOS (#733).
applyMacOsInitialMaximizedSizebuilt the native window at the work area, which tao keeps as the frame to restore to, so leaving maximized gave a screen-sized window pushed down by the title bar (0,-32 1710x1073→ now155,105 1400x900). The native window is built at the requested size;WindowStatestill holds the work area for the first frame.Unplaceable outer positions are rejected (#738).
TaoWindow.setOuterPositionrequires a finite position within ±1,000,000 dp and throwsIllegalArgumentExceptionnaming the values. On macOS such a value (a sentinel likeInt.MIN_VALUE) reachedsetFrameTopLeftPoint:, AppKit raised, and withpanic = "abort"the process died with onlypanic in a function that cannot unwind.Drag ghost off every display (#759). The ghost's position (pointer − grab offset) could land past the ±1,000,000 dp guard of #738 when the pointer left every display, and
setOuterPositionthrew on the Tao main thread.DragGhostWindowclamps its pointer-driven position (clampOuterPosition()); app-requested positions keep the strict guard.Wrap-content windows settle even when the scene re-measures (#759, #546). A re-measurement during the resize echo cancelled the effect before
WindowSizePolicy.settled, so the scene kept wrapping (TitleBar 138 px in a 300 px scene) and the initial centring was lost. The wait,settledand the centring now run in a separate effect keyed on the once-recorded resolved size.Headful suite runs to the end on every OS (#758, #759, #760). The #576 zoom present-lag case is skipped on Linux (the GTK host records no presented size); the #569 dialog-appearance and popup-placement cases no longer depend on the sampling clock, the wallpaper or one large display; the suite watchdog goes from 15 to 45 min (macOS never finished) and the job limit to 60 min.
One
TaoGpuRenderContextper surface (#752).rememberTaoGpuRenderContext()promised an identity-stable instance but built one per call site, so filament-compose created one engine per 3D view instead of one per window. The context is memoized per texture host (weak keys and values); a rebuilt GPU context still yields a new instance. In Zayit: 21 → 6 Filament engines, new-tab freeze 210–250 ms → 75–125 ms.XInitThreadsbefore GTK opens its display, Linux (#744). On libX11 < 1.8 (Ubuntu 22.04) Xlib only locks connections opened afterXInitThreads(), and rendering, input and GTK's loop share GDK's connection across threads: random[xcb] Unknown sequence numberaborts. Tao now calls it once beforegtk::init().launcher-linuxnever loses an earlyGMainLoopquit (#744).g_main_loop_run()ignores ag_main_loop_quit()issued before it runs, soregisterQueryHandler()followed byunregister()could hang inpthread_joinforever. The quit is now an idle source on the loop's own context. #744 also makes the pre-merge checks green again since the move toubuntu-22.04(theme e2e probe bounded, D-Bus tests skipped without a responsive bus, GraalVM jobs onsetup-nucleus's Xvfb, one flaky scroll test parked on CMP-10875).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/share-demo/examples/share-web-demoexamples/hot-update-demoscripts/windows-hot-update-e2e.ps1examples/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.SatelliteDragKind→WorkspaceDragKind,TabDropGhost.title→TabDropGhost.tab.macOS.appStoreandTargetFormat.isStoreFormatno longer compile. UsemacOS { pkg { appStore = … } }(a build script that branched onisStoreFormatreadsappStoreinstead); gate sandbox-sensitive code onExecutableRuntime.isSandboxed(), notisPkg().nativeDistributions { stripJreFonts = false }.onCloseRequest. A handler that never callsexitApplication(or closes its window) now vetoes the quit.PointerEventType.Scale*, no longer as two synthetic touches. Code pinching throughdetectTransformGesturesmoves toModifier.transformableor a Scale listener.setup-nodefrom CI and cache~/.gradle/nucleus/nodejs. Disable withnodejs { autoDownload = false }.NativeLibraryLoader.enableSingleInstanceis off in dev runs. Passtrueto test single-instance behavior fromrun.NucleusApp.appId. An app that calledFileKit.initbeforenucleusApplicationkeeps its own configuration; one that calls it insidecontentreplaces the automatic one. Opt out withinitializeFileKit = false.SEVERE. HookonUnresponsive/onResponsivefor UI, wrap known long synchronous work inexpectUnresponsive { }, or disable with-Dnucleus.tao.watchdog=false.versions\<version>\. Don't build paths from the install root. An app that shows no Nucleus window callsUpdateHandoff.signalReady()once it is up.TaoMouseButton.OTHERis now5(3/4areBACK/FORWARD). Recompile code that reads raw Tao button codes.TaoWindow.setOuterPositionthrows on NaN, infinite or out-of-range positions. Don't pass sentinels such asInt.MIN_VALUE.project.versionset,NucleusApp.versionnow reports it,./gradlew runincluded, so update checks run there instead of returningNotAvailable. Pre-release suffixes are kept at runtime and stripped only where jpackage /CFBundleVersionneed a numeric version.downloadUpdate()throwsParseExceptionfor a manifest artifact name that is not a single path component.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 windows25i4DockLandingRectTestwith the #695 split-side previewubuntu-22.04onGloballyPositionedoff the per-placement path (#560)--gc=G1on Windows and macOS from GraalVM 25.4titleBarslot onTabWindowsTabWindows' parts for app-owned tab windows,TabGripCursorXInitThreads, launcher-linux quit race)TaoGpuRenderContextinstance per surface