Nucleus 2.6 - #628
Draft
kdroidFilter wants to merge 307 commits into
Draft
Nucleus 2.6#628kdroidFilter wants to merge 307 commits into
kdroidFilter wants to merge 307 commits into
Conversation
xdg-shell gives a client neither its windows' screen position nor a way to place them, so the screen-space drag the satellite and tab workspaces are built on cannot work there: every window reports itself at (0, 0) and every move is ignored, silently. The gestures now ride the platform drag-and-drop session instead — the only pointer grab that crosses windows with coordinates. The source starts a session carrying an in-process token (SAME_APP only, so no foreign target ever sees it), the window under the pointer resolves the drop in its own coordinates and records it on the session, and the source acts on that record when the session ends. Chromium falls back to the same design on compositors without xdg-toplevel-drag, which GTK3 cannot reach: GDK3 exposes no accessor for a window's xdg_toplevel and creates the drag's wl_data_source inside gtk_drag_begin. The drag icon is a reduced snapshot of the dragged palette or panel, rendered through TaoWindow.contentSnapshot. On a floating satellite the header strip carries the gesture 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. Two leaks found on the way: - nativeLinuxHandles asked tao for the Xlib display handle, whose raw_display_handle_rwh_06 opens a fresh X connection per call and never closes it; polling the surface kind exhausted the server's client limit mid-suite. The kind is cached per window now, and the Xlib branch never asks for that handle. - gtk_drag_begin's pointer grab is released by GTK handlers that run after ours, so returning as soon as drag-end fired left every window of the application deaf to the pointer for good. The session drains GTK's queue and ungrabs the seat before returning. Covered by 26 headful cases on a real Wayland session — contract, lifecycle, concurrency, bursts, churn, edge cases — plus unit tests for the dock-zone math, the private payload and the transfer wiring, whose completion callback is the only thing that ends a session.
There the title bar carries two gestures: the header strip drags the palette into a dock over the platform DnD session, and the caption strip beside the window controls starts the compositor's window move. Nothing told them apart, so a press aimed at one landed on the other. The strip is now a rounded, tinted chip inset in the bar, brighter on hover — the same distinction Chrome's tab strip and GIMP's dock tabs draw, and for the same reason. Everywhere else the whole bar drags and the header stays flush, so the chip appears only where the split exists.
AppContext keeps its adapters in a Vec that adapter_index searches with binary_search_by, but push_adapter appended to the end — a search is only defined on a sorted slice, and adapters are created on the toolkit thread yet registered from the AT-SPI worker, so a registration landing out of order made every later lookup unreliable. Two call sites then unwrapped that lookup, and the crate is built with panic = "abort", so the miss took the whole JVM down: panicked at accesskit_atspi_common/src/adapter.rs:572: called `Result::unwrap()` on an `Err` value: 1 ... finished with non-zero exit value 134 Reported on the tab-satellites demo, where several windows are created at once, on both Wayland and X11. push_adapter now inserts at the position the search looked for, so the invariant holds, and the two unwraps skip the root announcement when the adapter is not in the context instead of panicking — which is what node.rs already does with the same miss (Error::Defunct). At worst one AT-SPI "window created" event is missing; the application stays alive. Not reproduced locally (nine runs under a client walking the tree), so this comes from reading the code rather than from a failing case. The patched library still exposes the demo's full AT-SPI tree. The vendored AccessKit had no patch list; vendor/accesskit-patches/ starts one, following the tao-patches convention and the PATCH(nucleus) markers at each site.
An Absolute position waited for `awaitMappedOnX11` before it was applied,
and the visibility effect shows the window meanwhile — so every positioned
window was mapped wherever the WM felt like and only then moved. On a
satellite that reads as the palette flashing at the screen's default spot
for a few hundred milliseconds before snapping beside its document.
The move is now issued before the show, which GTK and Win32 both carry
into the initial placement, and re-applied once the frame is real — that
still defeats the WM's own map-time placement and repairs an early move
that was lost, which is what the wait was there for.
Two anchoring bugs surfaced behind it:
- a satellite declared inside its parent's content composes in the frame
the parent window is created, so its anchor was resolved against a
frame of size zero and latched at `parentLeft + gap` instead of
`parentRight + gap`. It now waits for the parent to have a real frame,
and `anchoredOriginPx` returns null for a frame with no size so a
caller retries rather than latching;
- a satellite stepping back in after its parent left a maximized or
fullscreen frame was aligned against geometry the platform had not
finished changing, and every move it made while hidden was skipped.
The re-align now happens again on the first parent geometry that
lands afterwards.
`satellite anchors to the parent's right edge and follows it` has been
failing on a real GNOME session for weeks and was recorded as an
environment quirk; it passes now.
`SatelliteWorkspace.close` documents that a satellite's placement is kept until it is opened again, and it was — as the *rule* it was declared with. A palette the user had dragged somewhere came back at its anchor instead, and so did every palette after a `visible` sweep took them all down. Where a satellite is now becomes its placement the moment its window leaves composition, which is the last frame the live offset is known. The dock path already did this through `currentFloating`; this is the same capture on the way out of composition.
`SatelliteWorkspace.members` handed out the live observable list, so
`members == listOf(window)` was false even when that is exactly what it
held — Compose's list compares by identity — and the list could change
shape under a caller iterating it while a window opened. It returns a
snapshot now, like `TabWindowGroup.ids`, which documents the same trap.
`TaoMonitor.isPrimary` was false on every monitor under GDK's Wayland
backend, which names no primary at all, so `all().first { it.isPrimary }`
threw. Exactly one monitor now carries the flag: the one the platform
named, else the first — the fallback `TaoMonitors.primary` already
applied, moved to where the flag is produced.
The entry event carries a position, but only `onMoved` makes the scene root resolve the target under it — so the target a file drag entered on was not entered until the next motion event, and a platform that delivers enter then drop with nothing in between found no target and refused perfectly good files. The three hosts also each resolved the scene's drop target from their own expression. They now go through one lambda the window publishes, so an in-process driver reaches `TaoSceneDnD` along the path the OS takes rather than a parallel one that could drift from it — which is what makes the inbound half of drag-and-drop testable at all.
The suite goes from 148 cases to 250. What is new, by theme:
- **file drops** (14) — inbound drags driven through the same funnel the
platform callbacks use, so the synthetic transferable, the Compose
drag-and-drop tree and the app's own target all run: paths read back
through `awtTransferable`, drops clear of every target, a target that
refuses the drag, an empty payload, unreachable paths, 300 samples,
20 drops back to back, a drop crossing a live tab drag, and one aimed
at a window closing under it;
- **the two archetypes composed** (18) — one satellite workspace per tab
window, as in `examples/tab-satellites-demo`: a tab change redraws the
palette without recreating it, a torn-off tab arrives with palettes of
its own, a docked panel survives a tab change and outlives the tab it
was drawing, and a tab drag and a palette drag run at once;
- **the pointer** (16) — clicks, bursts of forty, sixty alternating,
sub-pixel drift, a wobble under the touch slop, real drag / tear-off /
merge gestures, the close button, the buttons that must do nothing,
and a pointer that leaves the window mid-drag;
- **placement** (4) — where a window is the first time it is *seen*,
sampled every 8 ms and failed on dwell rather than presence: a frame
of WM placement is nobody's problem, half a second is;
- **window extremes** (18) — transparent, one pixel across, 120 sizes in
a burst, an animation running through it, an embedded native view's
rect, a texture view with no source and one signalled 500 times;
- **load** (8) — four animating windows with palettes: no window
starved, anchoring converging while the owner never stops moving;
- **monitors and scale** (14) — the logical/physical round trips that
are only wrong on a HiDPI desktop, plus display hops;
- **races** (10) — background threads, coroutines on one group, a
restore between a tear-off and its window, everything closed at once.
Two mechanisms make this possible where `java.awt.Robot` cannot inject at
all (a Wayland compositor refuses the portal session): pointer events are
posted through `TaoWindow.dispatch`, the same entry the native loop uses,
and inbound drags through the window's drop-target lambda. Everything
above the JNI boundary is the real pipeline — the sub-pixel deadband, the
resize-edge band, hit-testing, the touch slop, the drag handles.
Cases that need something this host cannot provide report it: a second
display, or a native handle no test module can fabricate on Linux.
- Increase global watchdog timeout to 15 minutes for large test matrix - Normalize file drop paths using File.absolutePath on Windows - Fix asynchronous recomposition and geometry races in tab and satellite tests - Guard satellite window realign when unmaximizing parent - Focus target windows before AWT Robot injection under Xvfb/Openbox and Windows - Disable isAutoWaitForIdle in HeadfulRobot to prevent AWT event queue blocking
A cross-window gesture on native Wayland could freeze the whole
application: the pointer stayed stuck on the grab cursor and no window
received a click again.
`wl_data_device.start_drag` is validated against a still-pressed button
(Mutter's `meta_wayland_pointer_get_grab_info(require_pressed = TRUE)`).
With the button already up the request is silently ignored — no protocol
error, no grab, and so no `cancelled` / `dnd_finished` on the source.
GTK therefore never emits `drag-end`, and `start_outbound`'s cooperative
`while !done { main_iteration_do(true) }` spun for the rest of the
process's life. Tao's event loop never ran again, which is the freeze;
the pump kept painting, which is why the app still looked alive.
The gesture gets there whenever the client falls behind the compositor:
Compose crosses the touch slop on a queued motion that tao dispatches
after the real release was already delivered. Captured with
WAYLAND_DEBUG on tab-satellites-demo — the three working drags issue
`start_drag` with a press serial, the hanging one issues it 10 ms after
`wl_pointer.button(..., 0)`. A maximized window with satellites is the
easy way to see it, being the slowest to render.
Two guards, both in the Linux outbound path:
- `buttons_held` reads the seat's button mask off GDK's own device
state, and a session whose button is already up is refused before
`gtk_drag_begin` — Compose sees a transfer that did not start and
the gesture is a clean no-op.
- the wait loop is now bounded past the release: once no button is
held, a session that has not ended within a second is cancelled via
`gtk_drag_cancel`, which runs the ordinary teardown. Never bounded
during the drag, so holding a legitimate drag is unaffected.
Every other workspace case drives a gesture the way a user performs it —
begin, move, release, assert — which is how the intended behaviour is
pinned down, and also why those cases only ever visit states someone
thought of.
This one draws 200 actions from a seeded `Random` over one
`SatelliteWorkspace`: open / close a palette, dock / undock it, open and
destroy host windows, begin / move / end / cancel a drag, maximize,
resize, inject a scale change, flip the visibility sweep. Each action is
one call plus a 25 ms settle — it deliberately does not wait for a
steady state, because the states worth finding are the ones a gesture is
interrupted in. A quarter of the drag samples are what a coalesced flick
or a display unplug actually hands over: a point on no screen, or NaN.
What it asserts is not "the right thing happened" — for a random
sequence there is no such expectation — but that nothing is left
orphaned and nothing wedges:
- the workspace never names a window it does not have: no member is a
destroyed window, no satellite is docked into a non-member, no owner
or pin points outside the membership;
- no drag feedback outlives its drag, and no satellite keeps composing
in two hosts;
- native windows do not accumulate, and come back down to exactly the
quiesced set at the end;
- the closing phase asks for a plain state (visible, nothing docked,
one window) and it has to converge — a workspace that survived the
storm but can no longer be brought back is broken, it just fails
later, in the app.
Deadlocks need a watcher that is not on the loop. The driver runs *on*
`Dispatchers.Main`, so if the Tao loop and the dispatcher ever wait on
each other the driver stops too and cannot fail its own case: the suite
would just hit its deadline with nothing said. `MainLoopWatchdog` posts
heartbeats from its own thread and dumps every stack the moment one goes
unanswered for 8 s, which is the whole diagnosis; a late one is reported
as the worst stall and fails the case. Each action also carries a short
5 s budget, so a wedged action names itself.
A native panic cannot be asserted at all — a Rust `panic!` across JNI
aborts the process and no Kotlin frame survives it. Reaching the end of
the case is the assertion, and the seed is what makes an abort
reproducible.
Failures carry the seed, the workspace state and the last 40 actions;
`-Dnucleus.tao.headful.monkeySeed=<seed>` replays the action sequence.
It does not replay the run — the state each action lands on depends on
what the loop got done in the milliseconds before it — so the journal,
not the seed, is what identifies a sequence to promote into a case of
its own. A green run prints what it reached and fails if it reached
nothing, so the case cannot quietly stop testing anything the day a
guard starts refusing early.
Two palettes docked on the same side, one of them taken out: the one
that stays comes back with the content of the one that left. Its own
body — its `remember`s, its scroll position, its saveable registry — is
the one that got destroyed.
`DockSideStack` composed its panels in a `forEachIndexed` with no key,
so Compose identified them by their position on the side. Going from
`[first, second]` to `[second]` is not read as "first left" but as "one
slot fewer": slot 0 is kept and slot 1 disposed, so `first`'s subtree is
recycled for `second` and `second`'s own subtree is the one thrown away.
Found by the seeded monkey (`-Dnucleus.tao.headful.monkeySeed=424242`),
which reached it as a palette still composing a panel in a window it had
already been undocked from, while the palette actually docked there
composed nothing at all. The instrumented trace names the two halves:
+tools docked@1#17 tools docked, panel index 0
+outline docked@1#18 outline docked, panel index 1
+tools floating@11#19 tools undocked, floating window up
-outline docked@1#18 outline's panel is the one disposed
`key(entry.id)` around each panel, in both orientations. `Satellite` and
`Tab` already key their own declarations; this is the level below them
that was missing one.
`panelsOnOneSideKeepTheirOwnSubtree` pins it deterministically rather
than leaving it to the monkey's luck: two palettes docked right, each
body publishing the id it was composed for plus a `remember` marker,
the first undocked, and the survivor has to answer with its own id and
its own marker. It fails without the key (`live bodies {0=second,
2=first}`) and passes with it.
`gdk_window_get_frame_extents` answers with its `(0, 0, 1, 1)` placeholder
until the window is mapped and, under a reparenting WM, framed. Tao reads
the extents only from its `configure-event` handler, so a configure that
lands inside that window pins the placeholder in `outer_position` /
`outer_size` until the *next* configure — which on a software-rendered X
server under a lightweight WM (Xvfb + openbox, i.e. the CI Linux leg) is
seconds away or never comes at all.
Every consumer then reads a 1x1 window at the screen origin, which is
where most of the Linux headful failures came from:
- a tab torn into its own window measures "1.0 dp wide";
- a satellite anchored against a 1px-tall child centres its *top* edge
on the parent instead of its middle, and the follow logic preserves
that offset for good;
- `clientOriginPx` turns the placeholder into a negative screen
coordinate, so every AWT Robot gesture aimed through it lands
nowhere — eight cases timing out on "the drag started".
Fall back to the window's own frame origin plus its client size. Not to
`event.position()`: for a window a reparenting WM has framed, the
configure event carries coordinates relative to that frame, so using it
publishes every window at (0, 0) — which broke the window-v2 clone cases
on a real GNOME session.
`applyWindowOwnerRelationship` set `gtk_window_set_destroy_with_parent` on every owned window. That is the JDialog behaviour a dialog wants and the opposite of what a satellite wants: a satellite survives its owner, because the workspace hands it to another member when that owner closes. GTK took the toplevel down behind tao's back, leaving a live `TaoWindow` with no GtkWindow: a satellite that reported no geometry, could never be shown again, and — since `gtk_widget_show` re-realizes a disposed `GtkApplicationWindow` — crashed the process inside `gtk_application_window_real_realize`, dereferencing the menu sections dispose had already cleared. A SIGSEGV in `g_menu_model_get_n_items` with no GTK warning first; reproducible under Xvfb + openbox with the satellite monkey. `SatelliteWindow` now passes `destroyWithOwner = false`, and the show path refuses a toplevel GTK has destroyed on its own — the same window can still be reached through a `SetVisible` queued before the destroy.
Two ways the initial placement latched a wrong offset, both of them permanent because the follow logic then preserves whatever it captured. A frame of 1x1 is GTK's placeholder for "not laid out yet", not a size, so `> 0` was the wrong test: anchoring against a 1px-tall satellite centres its top edge on the parent rather than its middle. `reanchor`, `captureOffset` and `anchoredOriginPx` all go through `hasRealFrame` now. And one successful re-anchor is not enough. A window manager can report a real frame at the origin and apply the requested position several frames later — openbox under Xvfb takes tens of milliseconds, a loaded desktop longer — so the satellite was anchored to a parent that was never there and then trailed it by exactly the distance the parent moved after the map. Keep re-anchoring until the parent's frame has held still for three polls, which is the only signal a WM gives that placement is done, and stop as soon as a reparent swaps the anchoring: the satellite must stay where it is on screen when it changes owner. Stepping back in after a maximized or fullscreen stint has the same shape: re-showing the window races the single move the step-back path issues, and when the move loses the satellite stays where it was before it stepped aside. Re-assert the offset until it holds.
`clientOriginPx` put the whole outer/inner height difference above the content. Win32's `GetWindowRect` includes the invisible resize border below the content as well as beside it, so a pointer aimed through that model lands one border too low — enough to miss the bottom of a tab, and enough to shift every cross-window drop target. The bottom border is now assumed to match the side ones, which is exact for a plain resize frame and for a symmetric CSD shadow, and a no-op for a frame that adds nothing horizontally. `tearOff` had the mirror problem: it takes an outer frame and applied it as a window *content* size, so the new window came out one chrome too big. On Win32 that compounds — a tab dragged out, merged back and dragged out again gained the resize border every round. Size it from the source window's own frame-to-content difference instead.
Three ways the suite measured something that was not there yet. Every "mapped with a real size" gate accepted GTK's 1x1 placeholder, so a case proceeded against it and asserted on the placeholder: a torn-off window "1.0 dp wide", a robot gesture aimed at a negative screen coordinate. They all go through one `hasRealFramePx` now, which is `> 1` for the reason the helper documents. `composedIn` recorded one window per tab. A tab that leaves a multi-tab window is composed in *both* windows until the window it left renders again — Compose coalesces frames, so the two hosts genuinely overlap while a frame is pending — and the arriving host overwrote the departing one, whose disposal then erased the entry for a body that was still composed. It is a list of hosts, published for the body's lifetime rather than on every recomposition: a body that outlives its selection for a frame never recomposes again. `tab workspace never drops into a minimized window` ends with three windows covering the same strip, so which of them answers a point on it is decided by focus recency, not geometry. Wait for the platform to grant the focus the case asks for instead of assuming it was granted. The `#582` clipboard cases now skip when the window backend is forced onto XWayland inside a Wayland session: `wl-copy` owns the Wayland selection, so the two are on different clipboards and the case can only time out. CI has no `wl-copy` at all, which is why only the X11 leg run from a developer machine ever saw it.
… the aim Two follow-ups to the placement settle. It re-anchored on every poll until the parent's frame held still, which kept overriding a placement someone else owns: a satellite reopened where the user had dragged it came back at its declared anchor instead (macOS and Windows), and a palette whose owner window moved while the loop was still alive was re-anchored rather than left to follow. Only a parent frame it has not been anchored against yet is worth another move, and once the satellite has been placed the loop closes within ~190 ms — long enough for a window manager's map-time placement, short enough to be gone before the app moves anything. The robot-driven headful cases now say where they aimed and where the pointer actually went. Five of them still time out on the CI Linux runner and pass under the same Xvfb + openbox locally; "the drag started" never held is the same message whether the point was computed against a frame the platform had not published yet or the press never reached the window, and only the runner can tell us which.
The aim report already showed the pointer going exactly where the case asked on the CI Linux runner, so the coordinates are right and the press is not reaching the tab. The remaining two explanations are a client origin that maps the right screen point to the wrong point inside the window, and another window of the workspace sitting over it — so report the frame, the content size, the client origin those imply, the slot, and every group whose window covers the aim.
…ith it A case that fails between its press and its release leaves the mouse button down at the X server, and a `mousePress` on an already-pressed button is a no-op. Every later robot case then computes the right point, moves the pointer to it — the aim report proved that much — and receives nothing at all: no MOUSE_DOWN reaches the window. Five red cases on the CI Linux runner, four of them collateral, with nothing in the log to say which one was the real failure. The suite lets go of every button after every case now. The one real failure was `workspace satellite dragged by its title bar`: it grabbed 3 dp below the outer frame, which on a window whose frame adds nothing above its content — Tao on X11, Win32 — is inside the 5 px `ResizeFrameDecoration` band, so the press started a resize instead of the workspace drag. It only ever passed where the frame has a title bar of its own. The grab clears the band now; 3 px from the top edge of a palette is a resize grip, and rightly so. Under Xvfb + openbox, with the AWT Robot actually working (WAYLAND_DISPLAY must be unset for the JDK to use XTEST rather than the RemoteDesktop portal), the tab, satellite and workspace chunks are green: 155 cases.
Two cases were reading geometry the platform had answered but not yet filled in. A dock layout that has been placed once but is still a fraction of its final size answers a point near its right edge with the *top* zone — the nearest edge to its own centre is the top one when it is barely taller than the inset the case aims with. That is the macOS "the superseded drag stole the live preview: side=Top". Cases that aim at a named zone now wait for a layout whose edges are far enough apart to be told apart. `satellite placement` re-read the satellite's frame after sampling its trajectory, so a frame the platform had taken away again by then read as the screen origin and failed the case's own premise. The settled position comes from the sampling now, and samples with no real frame are not sampled at all.
`tab mouse click anywhere in a tab selects it` is the one robot case still red on Windows, and which spot it fails at moves with the client origin model — bottom edge before the frame's bottom border was accounted for, top edge after. Guessing a Win32 frame from a rect is how that happens; the case now prints the frame, the content size, the origin they imply and the slot, so the next run answers it with numbers.
The frame-extents placeholder has a size *and* a position, and only the size can be checked: `hasRealFrame` says nothing about (0, 0) being where the window is or where GDK had nothing better to say. Two ways that got in: `outer_position` was falling back to a substitute whenever the extents were the placeholder, and every candidate is wrong in its own way — `event.position()` is frame-relative under a reparenting WM, `root_origin` is implemented through `frame_extents` and answers the placeholder too, and `gdk_window_get_origin` names the client rather than the frame, so anchoring one window against another then mixes two rectangles. The size still comes from the configure event, which is what fixes the 1x1 frame; the position is simply kept as it was. And a `Moved` carrying a position that is not the one the follow logic asked for was taken for the user dragging the window, so the offset it had just computed was replaced by one derived from an intermediate frame — latched, and preserved from then on. While a move is in flight those are ignored, bounded by the same echo budget as before so a move the platform never confirms cannot make the satellite deaf to a real drag.
The geometry report answered the last red robot case on Windows: `aimed (87, 31)` against `outer=[26, 26, 576, 389] content=560x381 client=(34, 26)`, i.e. 5 px below the top of the content — inside the 5 logical px `ResizeFrameDecoration` claims on a resizable window. The press started a resize, not a selection. A strip sits flush with the top of its window and the band is right to be there: a press two pixels below a window's top edge is a resize grip in every browser too. "Hard against the top edge" of a tab has to mean the first pixel of it the tab actually owns, so the spot moves from an eighth of the slot's height to a fifth. It only surfaced once the client origin stopped putting a frame's bottom border above its content — before that the whole case aimed one border too low, which is what made its *bottom* edge the one that missed.
`CRobot.mouseEvent` segfaults the JVM on macOS when it is asked to release a button that was never pressed — the whole headful run died on its first case, before any robot case had run. The release after every case is now armed by the press, so a suite where nothing touches the robot never calls it.
Three defects the multi-window headful suite reproduces on Windows, all of them visible in a real app long before a test notices: Win32 only synthesises WM_PAINT once the message queue is otherwise empty, so a window animating flat out — each frame posting the next redraw as a queued user event — starves the paints of every other window in the app. They stop being scheduled for good: 1358 frames for the animating one against 3 for its neighbours, frozen until the animation ends. On Windows a redraw request is now answered as the queued event it already is, which puts every window on the same priority; OS-driven repaints still arrive as RedrawRequested, and the JVM-side latch keeps one request per frame. A satellite created in the very frame its owner is being taken down never hears that owner's closing announcement, so Win32 and GTK destroy it along with the owner. The composable is still declared and its remembered window is dead: the palette stays open, floating and invisible for the rest of the session. It now rebuilds against whoever owns it at that point. A parent window is reported at the platform's cascade position until its own WindowState is applied, so a satellite anchored to that frame is placed beside a window that was never there — and the stale placement its own WindowState carries lands *after* the settle loop's correction. Satellites now wait for two identical parent frames before composing, and the settle loop re-asserts the anchored offset until it holds. Test side: awaitUntil takes an optional detail lambda read at timeout, and the load and monkey cases use it to name the window that starved or the palette that never came back.
Answering a redraw request where it arrives re-enters rendering from inside the event batch, so `MainEventsCleared` — the tick that drains `TaoMainDispatcher` — is never reached and everything scheduled on the main dispatcher stops advancing. Collect the requests instead and serve them once per batch, right after that drain: every window still paints at the same priority, in request order, and a frame sees the work that produced it.
The extremes probe emits its frame ticker as a sibling of the content in the window's scene column, and a `fillMaxSize` there takes the whole height with it: the content every geometry assertion is about was laid out at zero, so the embed's rect and the composable's were compared stale against stale — eight of the eighteen cases were measuring nothing, and the embed storm failed outright on the one comparison the collapse could not satisfy. The ticker is now the smallest node that can draw, and `awaitSettledAt` asserts the content actually filled the scene rather than trusting the scene's own size.
A drop is answered by the topmost dock layout under the pointer, and the case's dialog is a dock host of its own whose default placement centres it over the parent window. On a display small enough for the two to overlap it sits astride the very zone the drags aim at and previews *its* top edge — the case is about two drags racing, not about which window is under them. It now parks the dialog off the parent's right edge and waits until it is clear of both drop points before starting.
A native image has no launcher .cfg, so the -Dnucleus.optimization.idleGc flag never reached it and idle GC stayed off. The plugin now also bakes the knob into nucleus/nucleus-app.properties; the runtime reads the system property first and falls back to that resource.
fix(optimization): enable idle GC in GraalVM native images
…me-polling fix(application): disable Compose system theme polling
…-default fix(application): disable single instance by default in dev runs
nucleusApplication calls FileKit.init(NucleusApp.appId) when FileKit is on the runtime classpath and the app has not initialized it itself. On Windows this makes FileKit.filesDir the %APPDATA% directory the NSIS uninstaller removes with deleteAppDataOnUninstall. FileKit is compileOnly: an app without it hits a caught LinkageError. Adds unit coverage and a process E2E (fileKitE2E).
- ensureProjectPackageMetadata no longer keeps an existing package.json, so a changed packageName reaches the npm name (installer name, NSIS app data dir) instead of staying stale. - Clear the read-only flag before deleting .app-image: jpackage's launcher is read-only, the copy keeps it, and Windows refused to delete it, so every repackaging failed with "Cannot delete ... after 5 attempts".
deleteAppDataOnUninstall only removed the names electron-builder derives from win.executableName and package.json. The GraalVM pipeline passes the image name there, so with a custom graalvm.imageName the %APPDATA%\<appId> directory (NucleusApp.appId, used for FileKit) survived the uninstall. The generated NSIS include now also removes %APPDATA%\<appId> from customUnInstall, under electron-builder's own condition (--delete-app-data, or deleteAppDataOnUninstall outside an update). The protocol registration shares that include, since NSIS allows one customUnInstall macro. Names that are not plain file names are refused, so the RMDir can never target %APPDATA% itself.
nucleusApplication(initializeFileKit = false) leaves FileKit untouched. Defaults to true. Covered by a new optOut scenario in fileKitE2E.
Auto-initialize FileKit and fix NSIS repackaging
The app no longer disappears while it updates on Windows. NSIS installs of
JVM apps are laid out as App.exe + app\App.cfg at the root and
versions\<version>\{app,runtime}, so installAndRestart installs the new
version next to the running one, launches it, and exits once the new
version's first window is on screen.
- plugin: versioned app image layout and NSIS hooks that neither kill the
running app nor delete its files when NUCLEUS_HOT_UPDATE=1
- updater: install while running, handoff on the new version's first frame,
cleanup of retired versions, classic fallback when the hot path cannot start
- multi-instance: cross-process install lock, pendingRestartVersion /
restartToInstalledVersion, explicit relaunchArguments
- fix: PowerShell update scripts are written with a BOM, so updates work for
accented profile paths (classic path included)
- fix: the post-update event is only reported when its target version runs
- E2E: examples/hot-update-demo + scripts/windows-hot-update-e2e.ps1
HotKeyListener ran on the native thread that received the press (Win32 message loop, X11/portal thread, AppKit), unlike every other native-callback module, which marshals through NucleusUiThread (#310). The listener is now resolved inside the posted block, so a press still queued when unregister() returns is dropped.
…read fix(global-hotkey): deliver hotkey callbacks on the UI thread
NucleusWindow.withFileKitDialogSettings { } fills FileKitDialogSettings.parent
from the window's platform identity: HWND on Windows, x11:<xid> on X11, an
xdg_foreign export held for the dialog's duration on Wayland. macOS stays
unparented (FileKit 0.15 only accepts an AWT parent there).
…ot-update # Conflicts: # plugin-build/plugin/src/main/kotlin/dev/nucleusframework/desktop/application/tasks/AbstractElectronBuilderPackageTask.kt
feat(application): parent FileKit dialogs to a Nucleus window
feat(updater): hot update for Windows NSIS installs
Compose 1.12.1 restores LocalSystemTheme as a deprecated public local typed as Compose's own SystemTheme, so ProvideNucleusSystemTheme provides that enum instead of Skiko's.
build: bump dependencies
The generator writes the before-install/before-remove paths in double-quoted YAML strings, which escape backslashes; the tests looked for the raw path and failed on Windows hosts.
Feed redirect (nucleus.updater.feedUrl / NUCLEUS_UPDATER_FEED_URL) to a local directory (LocalFileProvider), https or loopback http; UpdateSimulation for the update UI; installed apps honour both only with allowLaunchOverrides, and an unpackaged run never installs. The plugin forwards -Pnucleus.updater.* to run / runDistributable, adds serveUpdateFeed, and makes every packaging output a complete feed (manifest written without a publish provider, only this version's artifacts, stale manifests deleted). New updater-testing module with UpdateFeedServer, the fault-injecting loopback host the torture tests run on.
jpackage has no classpath option: it lists every file of --input, sorted by name, after the main jar. When two JARs define the same classes, the packaged app therefore loaded whichever sorted first, while ./gradlew run follows the runtime-classpath order. Jewel apps hit it: the IntelliJ icon libraries pull kotlinx-coroutines-core-jvm-1.10.2-intellij-2, which sorted before 1.11.0 and failed at run time with NoSuchMethodError (Job.cancel$default). The launcher .cfg classpath is now rewritten in classpath order right after jpackage, before macOS signing and the Linux pathing-jar collapse. The sandboxed strip task, whose output directory loses the order, records it in .classpath-order for the package task.
jewel-demo, scheduler-demo and system-info-demo compile to class file 69 but were packaged with the Gradle JVM's runtime, so the distributable died with UnsupportedClassVersionError. Resolve a 25 toolchain as jewel-tabs-demo does.
feat(tao): forward back/forward mouse buttons to Compose
…order fix: keep the Gradle classpath order in packaged launchers
feat(updater): test updates without publishing a release
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. 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, and jlink / App Store PKG / NSIS 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.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).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.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.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.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.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.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).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.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.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.4