Nucleus 2.6 - #628
Draft
kdroidFilter wants to merge 113 commits into
Draft
Conversation
Tao is now the single window backend. Removes the three AWT-based modules
(`decorated-window-awt`, `-jbr`, `-jni`) with their native sources, API
dumps, detekt baselines and GraalVM metadata, plus the `jni-demo` sample.
BREAKING CHANGE: `NucleusBackend`, `LocalNucleusBackend`, the `backend =`
parameter of `nucleusApplication`, `NucleusApplicationScope.backend` and
`NucleusWindowUnsafe.awtWindow` / `awtDialog` are gone, as are the AWT
overloads of `MaterialDecoratedWindow` / `MaterialDecoratedDialog` (M2, M3)
and `JewelDecoratedWindow` / `JewelDecoratedDialog` — only the
`NucleusApplicationScope` receivers remain. Compose Desktop's AWT `Window`,
`Dialog` and `Tray` are unsupported; use `DecoratedWindow`, `HostedWindow` /
`HostedDialog` and an AWT-free tray.
- nucleus-application: drops the AWT scope/window/dialog adapters and takes
`api(project(":decorated-window-tao"))`, since the backend is no longer a
consumer choice and a missing runtime module would only fail at launch
- material2: ports `MaterialDecoratedDialog` to a `NucleusApplicationScope`
receiver — it existed only in AWT form, so M2 keeps parity with M3
- removing the AWT overloads also retires the `LowPriorityInOverloadResolution`
/ `INVISIBLE_REFERENCE` workarounds they needed
- taskbar-progress-tao: single Tao dispatch path
- core-runtime keeps `WindowBackend.Awt`: it still describes a plain Compose
Desktop / Swing host embedding Nucleus libraries
- scheduler-demo moves to Tao (drops the `java.desktop/sun.*` add-opens);
service-management-demo swaps `java.awt.EventQueue` for
`rememberCoroutineScope().launch`, as SMAppService completion handlers must
hop to the Tao main thread
- drops the now-unused `jbr-api` catalog entry, the jni/jbr native build steps
and verify entries from CI, and rewrites the backend docs
- also adds `rect-stress-demo` / `widget-demo` to `apiValidation.ignoredProjects`
alongside the other demos, fixing their pre-existing `apiCheck` failures
Compose hardcodes grayscale as the Windows font-smoothing default. Enable ClearType end to end without runtime reflection: - plugin: LcdTextDefaultTransform (artifact transform + ASM) patches FontRasterizationSettings.PlatformDefault in ui-text-desktop jars on non-test runtime classpaths — SubpixelAntiAlias on Windows, opt-outs -Dnucleus.text.lcd=false (runtime) / -Pnucleus.text.lcd.patch=false (build). Android/HotReload/KMP guards mirror the CleanNativeLibs transform; referenced ctor/enum fields are verified so Compose layout drift fails the build. Canary test patches both the plugin's Compose and the consumer version from the root version catalog. - tao: lcdSurfaceProps attaches the OS-queried pixel geometry (cached, RGB/BGR, grayscale on any unknown) to opaque Windows window surfaces only; per-pixel-alpha surfaces (popups, NativeView overlay, Mica or Acrylic backdrops, transparent windows) keep unknown geometry so Skia falls back to grayscale. renderGlFrame now requires windowTransparent. - jewel-demo: use JewelDecoratedWindow instead of hand-rolled theming.
DecoratedWindow, DecoratedDialog, HostedWindow and HostedDialog take androidx.compose.ui.window.v2 state. Requested geometry is applied asynchronously; observed bounds/placement publish once the window is shown. tao-demo uses the v2 rememberWindowState and requestPlacement path.
…text feat(tao): LCD/ClearType text on Windows
WindowState.requestSize()/requestPosition() build the two-arg WindowBoundsProvider, whose getBounds dereferences an AWT-backed WindowGeometryProviderScope. Tao has none, so the request was dropped — but the placement was already rewritten to Floating, knocking a maximized window out of maximized for a request that never applied. Skip the whole request when the provider cannot be evaluated, and log it at WARNING (rememberWindowStateWithBounds hits the same path). The observed bounds now fall back to the native window rectangle when the v1 position never turns Absolute: a WM that emits no move event for a PlatformDefault window left WindowState.isInitialized false and bounds / size / position throwing forever. Also: narrow the constantBoundsOrNull catch to NullPointerException so a provider's own failure is not reported as "needs live metrics"; move dialog size clamping out of composition into an effect; move inspectableWindowBounds to dev.nucleusframework.window.tao so apiCheck covers it and the split package with compose-ui is gone; document that setMinimumSize/setMaximumSize clear per window, not per axis; register ComposeWindowV2BridgeTest with the scene test battery drift guard.
Compose v2 documents WindowState.bounds as the whole window, insets included, but the bridge published the v1 state's outer position paired with its inner size — so bounds.size changed meaning once the WM emitted its first move event, and requestBounds(state.bounds) or a WindowState.Saver restore resized the window by the decoration insets. Observed bounds now always come from the native outer rect, and the request path converts back to the inner size the v1 state expects. Hosts that never expose the TaoWindow (rememberSyncedWindowState) left isInitialized false forever on a window manager that emits no initial move, making every bounds / size / position read throw. They now publish an approximate outer rect instead. The initial v2 -> v1 conversion drains the request channels, so a window that left and re-entered composition before ever being visible fell back to the 800x600 platform default. Memoize the drained geometry per state. constantBoundsOrNull treated any NullPointerException as "this provider needs AWT window metrics", hiding real provider bugs behind a dropped geometry request. Only the shapes that come from the null scope we pass in count now. requestSize / requestPosition stay inert: their providers live in a synthetic lambda's captures, so honouring them would need reflection, and building a WindowGeometryProviderScope would need a displayable AWT window. Add requestInspectableBounds() as the working equivalent and point the diagnostics at it.
Compose 1.12's `androidx.compose.ui.window.v2` is anchored to AWT: `Screen` wraps a `java.awt.GraphicsDevice` and reads its insets through `Toolkit.getDefaultToolkit()`, and `WindowGeometryProviderScope` takes a `java.awt.Window` that must already be displayable. The Tao backend has neither, so every provider that touches the scope was accepted, logged and dropped, and `requestScreen` was drained into the void. Mirror the package instead, member for member, as `dev.nucleusframework.window.tao.v2`, backed by our own monitor enumeration and `TaoWindow` rather than by AWT. Migrating is a single import change, and deleting the package restores the upstream import unchanged if JetBrains decouples its own types. - `TaoMonitors` / `TaoMonitor`: multi-monitor enumeration via a new `nativeGetMonitors` on each platform bridge (`EnumDisplayMonitors` + `GetDpiForMonitor` on Windows, `NSScreen.screens` on macOS, GDK monitors on Linux), one tab-separated descriptor per monitor. Physical pixels, top-left origin, work area included — the conventions the existing primary-monitor calls already used. Never reports zero monitors. - `v2`: `Screen`, `WindowScreenProvider(Scope)`, `WindowMetrics`, `WindowGeometryProviderScope`, `WindowBoundsProvider` / `WindowSizeProvider` / `WindowPositionProvider` with their companions, `WindowState`, `DialogState`, savers and `remember*` factories. - `DecoratedWindow` / `DecoratedDialog` / `HostedWindow` / `HostedDialog` / `NucleusWindowHost` overloads for the cloned states. The host default bodies fall back to the v1 surface, so themed hosts keep working; the default host overrides them for the full path. - Size and position stay split instead of folding into a `DpRect` (`CombinedBoundsProvider`): a rectangle cannot carry an unspecified position or a wrap-content axis without turning both into `NaN`. - Wrap-content sizing routes through the window's own path rather than a one-shot content measurement, so `Unconstrained` / `PreferredWidth` / `PreferredHeight` keep re-measuring. The Compose-typed overloads stay as they are — best effort with the warning — and their KDoc now points at the clone. Verified headfully on a real window (`taoHeadfulTest`, 5 new cases): initial provider centring, `requestSize` / `requestPosition`, a scoped bounds provider reading live window metrics, `requestScreen` landing on the target monitor, and `screenId` tracking the hosting monitor.
Brings the 2.6 line up to date with the released one (#629 MSI installer options, #630 NSIS menu category, #632 clean-frame present skip, #633 alwaysOnTop stickiness). Conflict: `nucleus_tao_windows_deco.c` — 2.6's ClearType pixel-geometry probe and main's #631 topmost helpers were appended at the same spot. Both kept. Verified on Windows: rebuilt natives, `check` on decorated-window-tao and nucleus-application, headful suite 27 run / 0 failed.
Observed geometry was only published from an effect keyed on the v1 state, so a move or resize the window manager applies without the v1 state changing left `WindowState.bounds` / `position` / `size` reporting a stale rectangle for the rest of the window's life. The initial geometry apply is exactly that case: it lands after the effect has already run. Bump a counter from the window's own move / resize callbacks and key the publishing effect on it too. Both binders get it — the Compose-typed one has the same shape and the same gap. Caught by the headful suite, which only reproduced it with the full case list: the filtered run happened to settle in time.
Both conflicts are additive registries where 2.6 and this branch appended at the same spot: the JVM-only test list (its LCD capture test vs our monitor / bridge tests) and the headful suite registry (AlwaysOnTopHeadfulCases vs WindowApiV2HeadfulCases). Both sides kept. Verified on Windows: rebuilt natives, `check` on both modules, headful suite 32 run / 0 failed, twice.
… API Written blind on a Windows box and caught by CI. Three mistakes, all verified this time against the gdk 0.18.2 sources: - `gtk::gdk::prelude::DisplayExt` does not exist — `Display`'s monitor accessors are inherent in gdk3-rs. This is the E0432 that failed the build. - `Monitor::is_primary()` does exist, so the primary flag no longer has to be matched on geometry — which would not have compiled either, since `gdk::Rectangle` implements no `PartialEq`. - `Rectangle` is a boxed inline type, so selecting between the geometry and the work area *by value* moved a rectangle still read afterwards. Read the four numbers out first and pick between tuples. Also spell the tab/newline sanitiser as two `replace` calls: the char-array `Pattern` impl is newer than the toolchain floor this crate builds with.
`gdk::Display::default()` is `assert_initialized_main_thread!()`, and a failed Rust assertion crossing FFI aborts: the enumeration took the whole test JVM down with SIGABRT on a headless CI box (exit 134). Guard the no-window path with `gtk::is_initialized_main_thread()` and report "no monitors" instead, so a tray-only app or a unit test gets the synthesized fallback rather than a dead process. The X11 work-area fallback behind that synthesized monitor is already headless-safe (`XOpenDisplay(NULL)` returning NULL).
`LcdTextTest > Compose LCD text on an RGB surface has chromatic edges` has been failing the macOS tao-tests job since #626 merged, which leaves every PR targeting this branch red. Skia can only fringe where the platform font host produces subpixel glyph masks: DirectWrite and FreeType do, CoreText does not — macOS dropped subpixel antialiasing in Mojave and renders grayscale whatever the surface's PixelGeometry says. So `lcdScore == grayScore` there, which is this feature's documented behaviour (`macOS and Linux stay grayscale` asserts the same thing on the surface-props side) rather than a regression. Skip the pixel assertion on macOS only; Windows and Linux keep it.
`tao-headful (ubuntu-latest)` timed out on the centring case while the four other clone cases passed — so positioning works there; it is the *initial* position that openbox overrides with its own placement policy. The v1 path retries its Aligned centring for the same reason. Split the assertion: the size and the strict centre where the platform honours the request, and containment in the target work area on Linux. Prints the observed rectangle so the CI log carries the numbers.
The native move / resize callbacks bumped a snapshot-state counter, and those callbacks run on the event-loop thread from within the platform's resize handling — which can be *inside* a Compose measure/layout pass. The recomposition that write schedules then re-entered layout: `IllegalArgumentException: performMeasureAndLayout called during measure layout`, which took down the GraalVM headful battery on all three platforms. Signal through a conflated channel instead. A send carries no snapshot obligation, and the receiving coroutine resumes on the dispatcher once the native frame has unwound, so publication happens outside the pass.
`setMenu` reaches `g_bus_get_sync(G_BUS_TYPE_SESSION, …)`, which takes no
timeout: on a runner with no session bus it blocks forever. The test task then
never finishes and hangs the whole `preMerge` job until the 30-minute cap kills
it — the failure mode pre-merge.yaml's own comment records ("`:launcher-linux:test`
has done exactly that three times"), and it just cost another PR two runs.
Skip when neither `DBUS_SESSION_BUS_ADDRESS` nor `$XDG_RUNTIME_DIR/bus` is
there: without a bus there is nothing to register against anyway.
…ow hooks Rename DecoratedDialog's applyDialogOwnerRelationship to applyWindowOwnerRelationship and add its inverse, clearWindowOwnerRelationship, so a second secondary-window archetype can reuse the Win32 / AppKit / GTK owner plumbing. TaoWindow gains what a window that observes *another* window needs: setOuterPositionPx (physical-pixel positioning, SetWindowPos on Windows so a second-monitor DPI never leaks in), remove*Listener counterparts for the multi-cast moved / resized / destroyed / fullscreen-prepare hooks, and an onClosing hook fired at the start of requestClose() so owned windows can sever their owner link before the OS would take them down with it.
Add SatelliteWindow, the floating tool-palette / inspector archetype on Tao, with a Nucleus-level overload in nucleus-application: - WindowPositioner / WindowAnchor / WindowConstraintAdjustment: pure placement geometry with a flip → slide → resize cascade, pinned by 12 unit tests (registered in the scene battery and drift test). - Anchored initial placement, parent-relative follow in physical pixels with echo filtering, offset re-capture when the user drags the satellite, suppression while the parent is fullscreen or maximized, and SatelliteWindowState.reanchor() to re-apply the rule. - Reparenting keeps the satellite where it is on screen, including when the previous owner closes in the same frame: the owner link is severed before the old window is destroyed and the close decision is taken from composition, where the new owner is already known. - Headful coverage: anchoring + follow, maximize suppression + restore, reanchor, and reparent-as-the-owner-closes. The harness gains a selectable satellite owner, a closable dialog and onCloseRequest routing for that last case. - examples/satellite-demo: two document windows sharing one inspector.
…ble (#576) The outer gate exists to catch chrome drift — TitleBar and frame disagreeing. But the outer rectangle is a separate query from the resize event the scene tracks: on a loaded Xvfb the X server's geometry lagged the scene by 3px over two consecutive samples while `maxSceneVsInner` stayed at 0, and `SUSTAINED_FRAMES = 2` promoted that into a failure. Only the outer query can see such a lag; the scene has nothing to correct. Fail on outer drift only when the scene also lost the inner size. The metric line keeps reporting it either way.
Nine of the last nine `preMerge` jobs that hit the 30-minute cap — on main as much as on feature branches, two of them running the full 6 hours before the cap existed — were stuck in `:launcher-linux:test`. The culprit is `g_bus_get_sync(G_BUS_TYPE_SESSION)` with `DBUS_SESSION_BUS_ADDRESS` unset: GDBus then autolaunches, spawning `dbus-launch --autolaunch`, which waits on an X display a headless process never provides. No timeout, so the JNI entry point never returns, and with `nativeRegisterQueryHandler` the calling thread also sits in a condvar wait for a worker thread that is itself stuck there. Refuse to connect when the address is unset: a session bus that exists is always advertised through that variable, so "unset" means "no bus", and the bridge already treats a NULL connection as "launcher unavailable". This fixes the headless-app case too — a service or CI process must not spawn dbus-launch. Guard the second native test the same way (the quicklist one already was), and drop the `$XDG_RUNTIME_DIR/bus` probe from its check: that fallback is libdbus behaviour, not GDBus's.
`requestSize` and `WindowBoundsProvider(sizeProvider = …)` pair the size with `WindowPositionProvider.Current`. Before the window exists that was resolved against the placeholder rectangle the initial scope hands out, which pinned the window to an absolute point. The v1 `rememberWindowState(size = …)` idiom this replaces leaves placement to the window manager — keep that: an initial size-only request now resolves to `WindowPosition.PlatformDefault`. Once the window is up, `Current` reads the live outer rectangle as before. Review follow-up on #634; the other points (AWT on the Tao thread, dummy peer, dialog constraints, partial min size, host fallback, KDoc) were already addressed by the clone.
The Linux headful job timed out once on the initial size converging and passed the run before with the exact requested size; the diagnostic sat after that wait, so the log had nothing. Print the outer rectangle once a second during the wait.
Two X11 facts make it say nothing there. openbox applies its own placement policy to a client's initial position (the window lands at 0,0 — the v1 path retries Aligned centring for the same reason), so the centre is never observable. And this is the only headful case whose window receives an absolute position *before* `show()`: under Xvfb/openbox that window intermittently stays at GTK's unallocated 1×1 for the whole 15 s budget — the sizing trace shows it — while the very next window of the same run maps normally. That is a pre-map race in the v1 create → move → show sequence, independent of the clone, and not reproducible from a Windows box. Size, position and screen requests after mapping stay covered on every platform by the four sibling cases.
Compose's own `androidx.compose.ui.window.v2` types are no longer accepted by `DecoratedWindow` / `DecoratedDialog` / `HostedWindow` / `HostedDialog` or the `NucleusWindowHost` / `NucleusDialogHost` surfaces. On Tao that surface could only ever be half-working — every scoped geometry provider (including the ones `requestSize` / `requestPosition` build internally) and `requestScreen` were accepted, logged and dropped, because Compose's scope needs a displayable `java.awt.Window`. An API that silently ignores part of its contract is worse than one that does not exist; the supported v2 surface is the clone, `dev.nucleusframework.window.tao.v2`, where everything is applied and migrating is one import. Removed: `ComposeWindowV2Bridge`, the `ComposeWindowV2Access` friend-package accessor, the Compose-typed `DecoratedWindow` / `DecoratedDialog` overloads, `inspectableWindowBounds` / `requestInspectableBounds`, `rememberSyncedWindowState` / `rememberSyncedDialogState`, the matching nucleus-application overloads, adapters and host methods, and their tests. The geometry helpers the clone shared with that bridge move into `NucleusWindowV2Bridge`. `examples/tao-demo` and the host tests use the clone.
- `parentWindowMetrics` for dialogs: the `DecoratedDialog` overload captures its owner from `LocalTaoWindow` (the same capture the dialog uses for the native owner relationship) and hands it to the bridge, so `AlignedToParentWindow` resolves instead of reporting a missing parent. - Initial outer size: a v2 provider returns the outer rectangle, but before the window exists its insets are unknown, so it was applied as the inner size and a natively decorated frame came out larger by its chrome. Once mapped, the bridge measures the insets and — if the size is still the initial request — shrinks the inner size by them. - `measureWindowContent` is now a real measure pass: the scene host registers a per-window hook onto `ComposeScene.measureContent(constraints)` (`TaoContentMeasurers`, same shape as `WindowSizePolicy`) and the geometry scope calls it, converting through the window's scale. The clamped current size remains only for the pre-window / no-window cases. - X11 pre-map race: an absolute position applied before the window is mapped intermittently left it at GTK's unallocated 1×1 for good under Xvfb/openbox. The v1 Absolute path now waits for real outer bounds on Linux (≤1.5 s) before moving, the way Aligned already retries. The centring headful case runs on Linux again (size + containment; the WM still owns placement).
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.
feat(tao): satellite windows, a docking workspace, and Chrome-like tabs
…640) `PeekMessageW` delivers pending cross-thread *sent* messages inline: the kernel re-enters the window procedure through `KiUserCallbackDispatcher` before the peek returns. Two non-reentrant `parking_lot::Mutex`es were held across such a peek, so the nested dispatch deadlocked the event-loop thread against itself — it parked in `WaitOnAddress` and never pumped a message again, leaving the window permanently "Not Responding". - `event_loop.rs`: the keyboard callback held the global `KEY_EVENT_BUILDERS` map across `KeyEventBuilder::process_message`, which peeks. The builder is now taken out of the map for the duration and put back afterwards (only if the slot still exists, so a window dropped re-entrantly is not resurrected). Its map value became an `Option` slot to make that take/put-back possible. - `keyboard.rs`: the key-press and key-release arms held `LAYOUT_CACHE` across their peek, while every other arm — and `update_modifiers`, reached from any mouse or focus message — locks it too. The guard is now scoped to end before the peek and re-acquired after it. Reported as a freeze when moving a window between Windows 11 virtual desktops: that path is keyboard-triggered and makes the shell send messages to the window mid-peek. The reporter's stack shows the two nested window procedure chains around `NtUserPeekMessage` and the park in `WaitOnAddress`. Only injected or sent key messages can nest here — real keyboard input is posted to the queue rather than sent, and the peeks use `PM_NOREMOVE` — so a nested key message now finds an empty builder slot and is dropped instead of hanging the process.
Two more instances of the #640 bug class, both fixed upstream and ported by hand — a non-reentrant lock held across a call that pumps or synchronously sends messages, so a nested window-procedure dispatch re-locks it and the event-loop thread parks in `WaitOnAddress` for good. - IME: the window-state lock was held across `ImeHandler::process_message`, which peeks the queue on `WM_IME_ENDCOMPOSITION` (the Korean IME sends the committed string after the end message, so the queue decides whether to defer). The peek now happens in `ime::peek_commit_queued` before the lock is taken, and the result is passed in — mirroring upstream, where "IME character-pending detection happens before acquiring window state locks". Wider blast radius than #640: there only the keyboard arm took the lock, here nearly every arm does, so any nested message deadlocked. Upstream: tauri-apps/tao#1215. - `TaskbarCreated`: the window-state lock was held across `set_skip_taskbar`, which goes through `CoCreateInstance` + `ITaskbarList`. An STA COM call pumps the message queue while it waits, so the window procedure is re-entered. The flag is now read out before the call. Upstream: tauri-apps/tao#1264. `Imm32Source` carries the precomputed flag instead of peeking on demand; `ImeSource` is unchanged, so the IME state-machine tests keep driving the same sequences through their stub.
Spamming requestPlacement faster than the OS animation and then asking for bounds left the window maximized on macOS, with the bounds request silently dropped: `window v2 clone: rapid maximize/restore toggling then a bounds request converges` timed out at 30 s, 100% reproducible on one machine and intermittently on CI. Two defects, both needed for the failure. The bridge decided whether it had a placement to leave from a single sample of `v1.placement` plus the native flags. AppKit clears `isZoomed` at the *start* of the un-zoom animation, so right after a toggle both read Floating while a queued `zoom:` has not landed yet. `leftPlacement` was therefore false, the bounds were applied to a window that was about to re-zoom, and `confirmBounds` — the whole point of which is to catch exactly that — was skipped because it is gated on the same flag. A placement applied within the last second is now its own reason to confirm. `confirmBounds` could not have recovered anyway. It returned as soon as the geometry matched, without ever asking whether a placement had come back, and its size check cannot detect one: a maximized window ignores v1's size, so `decorationInsets` derives the insets from a target that never landed (2560 outer - 820 target = 1740 dp of "decoration") and the subtraction reports sizeOk for any outer rectangle at all. It now treats a re-asserted placement as a failure to converge in its own right, tested before the geometry, and clears it by handing Floating back to v1 so the window composable's placement effect issues the single `zoom:` — the same "who issues the restore matters" rule the bounds path already follows, and the reason poking the native flag here would re-zoom. The case now passes in 1.9 s. Full headful suite on macOS: 205 run, 0 failed. Also give that wait a `detail` snapshot, since a bare "timed out" is what made this expensive to diagnose in the first place.
…ggle-storm Tao: converge a v2 bounds request after a placement toggle storm
The Metal and GL scene hosts never purged Skia's GPU resource cache; only the Windows host did, off the resize path (#347, #477). This brings macOS up to that level and pulls the shared policy into one file. Measured first: Ganesh already hands out 256 MiB by default, exactly the value the Windows host writes at attach. So that write was a no-op and the comment claiming it "forces purgeAsNeeded on every flush ... which is what keeps the steady state bounded at all" was wrong — Skia purges to fit its budget whether or not we set one. What reclaims is the purge. The comment is corrected rather than propagated. macOS gets the same mechanism, adapted to the backend: - the budget is anchored inside the same runOnRenderThread hop as makeMetal, since writing it purges to fit and so belongs on the owning thread; - purgeGpuResourceCache() submits the limit-toggle to the render executor instead of awaiting it — Metal has no current context, so none of the #514 foreign-context hazard applies here, but the context is thread-affine, and blocking the Tao main thread would park the drag behind the in-flight replay; - onResizeStreamAdvanced() purges every 250 ms while sizes stream and once more 500 ms after the last one, standing in for the WM_EXITSIZEMOVE macOS never sends. One deliberate divergence from Windows: the settle's System.gc() is gated on the burst having carried at least 8 resize events. Windows only sees WM_EXITSIZEMOVE after a real drag, but here every size change settles, including the single event a zoom, a snap or a programmatic resize produces, and a stop-the-world collection after each of those costs more than it returns. The Windows resize path is untouched beyond the constant move and the comment fix.
The settle pair (a 500 ms timer standing in for the WM_EXITSIZEMOVE macOS never sends, then a purge and a System.gc()) does not pay for itself on this backend. macOS paces its resize frames through the display link, so it never accumulates the way Windows' unpaced modal loop does: a 60-step resize storm on tao-demo moved the graphics footprint 68 MB -> 72 MB, and a purge + GC at the end of it returned essentially none of that. Against that nil benefit sits a real cost — a full cache purge re-mints the glyph atlas and layer backings, and the GC is stop-the-world, both landing half a second after every resize, including the single event a zoom, a snap or a programmatic resize produces. Keep the in-drag periodic purge, which is free (every frame of a drag is re-rastering anyway) and bounds a long drag on a large display. Drop the settle timer, the GC nudge and the burst-count gate that tried to make the nudge affordable; with them go the two constants and the host scope they needed, so startRenderLoop goes back to its local scope. The reclaim #638 actually wants is at rest, not at drag end, and belongs on the idle path.
Port of 4126663 to 2.6, extended to the branch's own window openers. The window and dialog openers had their composition target inferred, so a single non-UI-targeted composable called in the `nucleusApplication` scope (the Compose compiler bakes `@ComposableTarget` onto any unmarked factory forwarding a target-marked content lambda) reclassified the whole scope — nested windows included — and every `@UiComposable` call in it warned, which is fatal under `-Werror`. Declare each opener `@ComposableOpenTarget(-1)` with `@UiComposable` content lambdas, the way Compose Desktop's own `Window`/`Dialog` are open: they are callable from any applier and always compose UI content in the new window's own composition, so neither direction of the applier leak survives. Covers `DecoratedWindow`/`DecoratedDialog`, the `NucleusWindowHost` / `NucleusDialogHost` interfaces with their default implementations, `HostedWindow`/`HostedDialog`, `SatelliteWindow`, `Satellite`, `Tab`, and Tao's equivalents plus `TaoStandalonePopup`. `TabWindows`, the v2 `DecoratedWindow`/`DecoratedDialog` and `DragGhostWindow` already infer the same scheme (`[_[UiComposable]]`), as do the Material/Jewel wrappers. Once a declaration carries an explicit target, inference stops for all of it: every composable lambda parameter needs the annotation, not just `content`. `Satellite`'s `floatingContentWrapper` was the one case where leaving one unmarked kept dragging the caller's applier into the satellite content. `COMPOSE_APPLIER_CALL_MISMATCH` is only a warning, so both test compilations escalate it to an error and a fixture per module compiles the reported shape (non-UI factory in the application scope, UI content in every window).
…-target-window-scope fix(application): keep window content on the UI applier (#636) [2.6]
#569) `nativePopupLayers` made every Compose `Popup` a real OS window, but the *decision* of where to put it stayed window-rooted, in two stacked ways. The layers each built a work-area-sized `WindowInfo` so a popup could lay out and flip against the display — and then `setContent` replayed the owner window's composition locals over it, so `Popup.skiko.kt` read the window's `containerSize` and clipped every popup back inside the window. The intended design had never taken effect. The layers now re-provide `LocalWindowInfo` inside the replayed locals. With that in force the box is screen-sized but still rooted at the window's content origin, so a `DropdownMenu` in a window near the bottom of the display did not flip up — Compose saw a whole work area of room below the anchor — and walked off the screen. Each layer now clamps its native frame into the work area of the display it lands on, at the single point where it pushes that frame: `popupScreenClampOffset`, fed the owner's content origin on screen plus every display's work area. Only the native frame moves; `boundsInWindow` stays what Compose believes, which is what hit-testing and the surface content are expressed in. Re-clamped on every push, so an open popup survives an owner drag, across monitors included. Dialogs go through the same layers but must not follow the display: `Dialog.skiko.kt` places at `containerSize.center`, so a window-owned dialog centred on the screen would sit visibly off-centre and drift as the window moved. Layers report the window size for dialogs and the work area for popups, discriminated on `scrimColor` — only `Dialog.skiko.kt` writes it, from `DialogAppearanceController.properties` during `DialogLayout`'s composition, before `layer.Content { }` reads the container. macOS needs the NSView's own origin on screen (a native title bar sits between it and the window frame), hence `nativeGetContentRect`. Wayland reports no geometry and is left unclamped: a popup there is a `wl_subsurface` placed relative to the parent, with no global position. Also exposes `nativePopupLayers` on `JewelDecoratedWindow`, which had no such parameter at all — Jewel apps could not opt in. Jewel needs nothing further: `DefaultPopupRenderer` delegates to `androidx.compose.ui.window.Popup`, so its combo boxes, menus and tooltips flow through the fixed layers. Tests: 18 unit cases on the clamp geometry, and 13 headful cases driving real windows parked at real work-area edges — including one that reads the popup HWND's rect back through Win32 and asserts it matches the reported frame to the pixel, and two that pin the dialog contract. A new "Popups" tab in nucleus-demo parks the window at any corner and opens menus anchored at each window edge.
A Compose Dialog opened through nativePopupLayers had no scrim, clipped its shadow and its appearance animation at the layout edge, slid diagonally towards the display centre while scaling in, and stayed put when the window was resized. - Scrims: the owner window paints every layer's scrim after its content and each layer paints the scrims of the layers above it (PopupScrimRegistry, TaoSceneBundle.renderOverlay). A scrim change marks the owner scene visually dirty, so the Windows clean-frame present skip no longer eats the fade. - Draw margin: the native surface extends 32 dp past boundsInWindow so shadows and the 10 dp slide-in are not clipped. Compose 1.12 renders a scene as one RenderNode drawable with unbounded bounds, so the R-tree cull-rect measurement upstream uses reports the whole canvas; a constant margin replaces it. The screen clamp is decided on the content rect; the interactive region stays the content (Windows content rect, macOS region hit-test, Linux press filter). - Dialog scene size: a dialog's root Layout fills the layer scene's constraints and carries the appearance GraphicsLayer, so its scale pivots on the scene centre. Dialog layers now run their inner scene at the owner window size, popups keep the work area. - Resize: the dialog container size is read from the owner's snapshot-backed WindowInfo, so the dialog re-centres when the window is resized. DialogAppearanceHeadfulCases films both layer modes with java.awt.Robot and compares slide-in, scrim ramp and settle time; PopupFrameRecord gains the content frame next to the inflated native frame.
The appearance film now also records the hide animation and counts grabs that repeat the previous frame during either animation — dropped frames show up as a stall count the native layer must not exceed. The owner window carries forty rows of text so its per-frame present costs something. The first-visible check is one-sided: the native surface legitimately shows its first frame before the owner's next present.
…t fades out (#569) Dialog.skiko.kt's disappearance swaps the layer's content for an empty Layout that replays the recorded picture, and Compose then reports a zero-size boundsInWindow at the window centre for the whole fade-out. An in-scene layer draws into the window canvas and does not care; the native surface followed the bounds and shrank to a 32 dp square around a point, so a closing dialog collapsed and vanished instead of fading out. Each layer now sizes and places its surface on the last non-empty bounds. The appearance film gains the Material 3 AlertDialog nucleus-demo opens, a warm-up before filming, a duration-based comparison, and the smallest height the dialog spanned while fading out.
…face
The Windows and Linux context menu flyouts drew inside the window's render
target whenever the window ran without nativePopupLayers, so a menu opened
near an edge was clipped by the window like any in-scene popup. An
OS-looking menu has to leave the window like the menus it imitates, and the
application's choice for its other popups must not decide that.
nativePopupLayers is a whole-scene switch (platform vs canvas layers), so a
per-popup opt-in needs the seam Compose 1.12 actually uses: Popup picks its
layer through LocalComposeSceneContext. A friend-package Java accessor
reaches that internal local without reflection; NativePopupLayers { } then
provides, for its subtree only, the window scene's own context with
createLayer routed to the window's native popup layer factory — the same
factory attach() uses when nativePopupLayers is on. The context menu
representation wraps the Windows and Linux flyouts in it; macOS stays on
NSMenu.
…tates Three things the menu got wrong on a Linux desktop, found by driving a real right click against a nested GNOME Shell and reading back both screenshots and the app's own trace (scripts/context-menu-wayland-e2e.py, with the fixture it drives in nucleus-application's tests). A menu opened near the bottom of the screen was cut off. On native Wayland a client cannot know where its own window is, so it cannot keep a popup on screen by itself — the X11 clamp of #569 has nothing to work with there. The popup layer now maps as an xdg_popup instead of a wl_subsurface and lets the compositor place it: it flips above the pointer when there is no room below and slides along an edge, which is what GTK's own menus do. The tao patch carries the anchor point, the surface size and the shadow margins in one request, because GDK builds the positioner from the window's geometry as it stands at map time — a popup still sized 1x1 there asks the compositor to constrain a 1x1 rectangle and is never flipped. One popup per parent takes that path (an xdg_popup must be its parent's topmost popup); a dialog keeps the subsurface, since it belongs to its window rather than to the display. A second right click only closed the menu instead of moving it. The press that dismisses a popup is delivered to the scene in the same turn as the dismissal, so Compose's contextMenuOpenDetector — disabled while the menu is open — was still disabled when the press arrived, and the press did nothing. The host now recomposes and re-lays-out the scene between the two, so the detector is listening again by the time it sees the press. The menu also appeared a beat late: the layer painted its first frame only on the owner window's next redraw, though that first render is what measures the content and puts the popup on screen at all. It renders as soon as its GPU side is up. Measured against the fixture's trace, press to first present is now 40 ms steady, 108 ms for the first menu of a session.
The flyout drew its shadow with Modifier.shadow and the themes passed the OS box-shadow alphas (Adwaita 9 % / 5 %) as ambientColor / spotColor. Compose desktop multiplies those alphas by its fixed elevation factors (0.039 ambient, 0.19 spot), so the menu on GNOME darkened the pixels next to it by about 1 % — measured on the E2E screenshots — and read as having no shadow at all. An elevation shadow also cannot reproduce a CSS box-shadow, which is how GTK, Breeze and Fluent all describe theirs. The themes now carry those declarations as box-shadow layers (offset, blur, spread, colour), drawn as the menu's rounded rectangle under a Gaussian mask with the CSS standard deviation of half the blur radius: libadwaita's _popovers.scss for Adwaita, Breeze's ShadowLarge for KDE, Fluent 2's shadow16 token for Windows. On the nested GNOME Shell the bottom edge now darkens the backdrop by about 10 %, tapering over 17 px, as the GTK menus next to it do.
…eb token The Fluent flyout took its shadow from Fluent 2's shadow16 token, and got that wrong too (its ambient layer is 0 0 8px, not 0 0 2px). The menu imitates the Windows 11 context menu, whose shadow is WinUI's ThemeShadow at Translation.Z = 32: GetDropShadowRecipe gives a single directional layer, blur radius 16 (+1) shifted down 8, at 0.14 in light and 0.26 in dark, and no ambient layer at that elevation. The composition blur radius is the ~3 sigma extent WinUI reserves around the caster, so it becomes an 11 dp CSS blur.
Checked field by field against MenuFlyout_themeresources.xaml and
Common_themeresources_any.xaml in microsoft-ui-xaml. The flyout had the touch
metrics and a few guesses; a right click opens a MenuFlyout with the mouse,
and GetShouldBeNarrow then puts every item in its NarrowPadding state.
item row 36 -> 28 (MenuFlyoutItemThemePaddingNarrow 11,4,11,5 around a
14 px label), with the 4,2,4,2 MenuFlyoutItemMargin the rows
had no vertical part of
label inset 12 -> 11
presenter MinWidth 168 -> 96 (FlyoutThemeMinWidth), no MaxWidth,
padding 4 -> 0,2 inside the 1 px border
separator 12 px insets, 4 px above and below -> edge to edge, 1 px
chevron E76C -> E974 (ChevronRightMed), 12 px, 24 px from the label,
TextFillColorSecondary rather than the label colour
shortcut CaptionTextBlockStyle 12 px, 24 px gap, margin 24,4,0,0,
TextFillColorSecondary
colours the bound resources themselves, translucent where WinUI's
are: TextFillColorPrimary/Secondary/Disabled,
SubtleFillColorSecondary for pointer-over,
DividerStrokeColorDefault, SurfaceStrokeColorFlyout
The presenter border is BackgroundSizing=InnerBorderEdge: the ring is outside
the background and blends with what is behind the menu. The flyout now paints
it that way for every theme, which is also how libadwaita's 0 0 0 1px
box-shadow ring works; Breeze strokes over its own fill, so its border colours
are pre-composited and its menu padding drops by the ring it now sits inside,
leaving its pixels as they were.
The chevron and shortcut colours move from per-theme alphas into the colour
set, since WinUI's differ between light and dark. The acrylic backdrop is not
reproduced: the surface is the brush's FallbackColor.
…er it is Six findings from a review of the #569 branch, each reproduced by a test that fails first. The margin a native popup layer's surface carries past `boundsInWindow` is transparent, but on Linux it is still the popup's window as far as the display server is concerned, and the layer swallowed everything that landed there: a click on a button 20 px beside an open menu closed the menu and never pressed the button, and hovering past the menu's edge froze the owner's hover state. Windows and macOS get the pass-through from the OS, which is handed the *content* rect. GTK's own input shaping does not take on a popup toplevel — the region reaches GDK and the X window keeps its full input shape — so the layer routes the event to the owner itself (`TaoPopupHostLinux.forwardMarginPointer`), and only while the point is over the owner's content: a press over another application is not ours to deliver. The press path is the one an owner press already takes, dismissal and mid-turn recompose included, now shared as `dismissPopupsBeforePress`. `NativePopupMarginInputHeadfulCases` drives a real pointer with `Robot` against a real popup and asserts on what the owner window's scene received — including a case with no popup open, without which "the owner saw nothing" would be as consistent with a broken driver as with a swallowed press. The macOS layer recorded its picture with a cull rect rooted at the picture origin while the scene draws in owner-window coordinates, so the rect the replay matrix maps lands off the drawable. Skia unrolls a one-op picture and never consults the rect, and a Compose scene is exactly one op, so a bare popup survived it; a popup dimmed by a dialog above it does not — the scrims go into the same picture and the whole frame is quick-rejected. `MacPopupPictureCullTest` runs the layer's frame against a real scene through the production record and replay paths and reads the pixels back. A compositor-placed popup (`xdg_popup`) that re-measured after it was mapped resized its EGL buffer while the `xdg_surface` geometry stayed at the anchored size — the buffer/geometry disagreement of #502. GDK positions a popup once, so the layer re-maps instead: hide, re-anchor at the new size, show. Also: closing a layer that was still dimming left the owner window dark until an unrelated invalidation, because `PopupScrimRegistry.unregister` removed the entry without reporting the change; the popup screen clamp read `TaoMonitors.all`, which invents a 1920x1080 monitor at the origin when the platform names none, and would have dragged a popup onto a display that does not exist — it asks `reported` now, and treats empty as "no geometry"; and nucleus-demo was missing a trailing comma, which failed `ktlintCheck` and took the whole `tao-headful` job down with it.
…reen-clamp fix(tao): place native popup layers against the screen (#569)
…inst `examples/tao-native-test` compiles decorated-window-tao's test suites into a native image, but `taoTestArtifacts` published only the classes, so every dependency of that test source set had to be repeated in the consumer. Material 3 was not, and the first case that reaches an `AlertDialog` throws `NoClassDefFoundError: androidx/compose/material3/MaterialThemeKt` **on the Tao main thread**, which closes the loop and fails the whole `test-graalvm` job on all three platforms. The configuration now extends `testImplementation`, so a dependency added to the suites reaches the image without being repeated — reproduced and verified on the JVM (`:examples:tao-native-test:run --args=headful`), which shares the classpath. While the film cases were finally running, three things they had never been green on: - Their content is composed into a `ColumnScope` next to the harness's default `fillMaxSize` background, so it was laid out at zero height: the forty rows of text that exist to make the owner's per-frame present cost something rendered nowhere. `paintDefaultBackground = false`. - The grabber started *before* the warm-up and spent its whole frame budget on it, leaving the curve with nothing after the timestamp it measures from — reported as "the dialog never showed up on screen". - One capture session covered both halves, so the appearance consumed the budget and the disappearance got no frames at all, which is what the comparison cases were failing on. Each half films in its own session now, within the same total budget. Under Xvfb + openbox the `appearance` series goes from 6 failures to 0. The `native popup layer matches the in-scene layer` comparison is borderline there (slide-in measured 15 px vs 10 px against a 4 px tolerance) because a 6 ms sample period is coarse for a 10 dp slide; the tolerances are left as calibrated. Not addressed: `graphicsLayer translation filmed — native popup layer` hangs on Linux, which is what makes that leg reach the 900 s watchdog. It draws a `GraphicsLayer` created from the *owner window's* `GraphicsContext` inside the popup's own GL context, with alpha forcing the saveLayer path; the in-scene variant of the same case passes. Pre-existing and its own investigation.
…-material3 fix(tao): give the GraalVM test image the classes it was compiled against
Third and last host of the shared policy from GpuResourceCache: the EGL
host now anchors the same budget at attach and purges the per-size
scratch periodically while resize events stream, like Windows inside the
modal resize/move loop and macOS on the display-link-paced one.
The purge is armed in onResized but performed in the render pass. That
is not a detail: onResized runs on the event-loop thread with no EGL
context bound and the swap thread may be holding ours for its
eglSwapBuffers, while the render pass is the one point where this host's
context is current on this thread. It also lands right after
applyPendingNativeResize has closed the previous size's Surface and
BackendRenderTarget, which is exactly when their backing memory is
unlocked and the toggle has something to return.
Unlike macOS, the Linux resize path IS where the memory sits. On a
60-step storm, then a second storm shrinking back through the same
sizes (NVIDIA graphics memory for the process, native Wayland / GNOME):
without purge 13 MB -> 106 MB -> 114 MB
with purge 13 MB -> 71 MB -> 27 MB
Without the purge the footprint ratchets: the second storm revisits
sizes the first already paid for and still grows, because nothing ever
releases the scratch of a size no frame will ask for again. With it the
footprint tracks the current window instead of the high-water mark of
every size ever seen. Same shape on the X11 attach path (112 MB -> 23
MB). Frame throughput is unchanged in both regimes (~1050 frames per
1000 ms on Wayland, ~93 on the vsync-paced X11 path), so the reclaim
costs no frames.
No settle purge and no System.gc() nudge, for the reason macOS has
none: GTK offers no drag-end signal, and a timer standing in for one
buys a stop-the-world collection after every zoom, snap and
programmatic resize.
Refs #638
…cache-purge Tao: reclaim the GPU resource cache on the macOS and Linux hosts
`graphicsLayer translation filmed — native popup layer` never returned on Linux/X11 and the 900 s watchdog took the whole suite down with it. The hang was not the cross-context GraphicsLayer: the case was the only one calling `Robot.createScreenCapture` on the Tao event-loop thread. The JDK's Linux Robot grabs pixels through GTK, and loading GTK from AWT calls `gdk_threads_init()`, which retroactively installs GDK's global lock in the process — GDK then holds it around every event it dispatches on the loop thread. The capture takes the same non-recursive mutex through `gdk_threads_enter()`, so a driver resumed from inside a GDK dispatch parks its own thread for good. Timing decides whether the case is inside a dispatch, which is why it passed alone and hung after the in-scene variant. - HeadfulRobot.capture: screen grabs run on an IO thread under the same timeout and unavailability latch as gestures - translated(): capture through it and fail the case, not the suite - watchdog: dump every thread's stack before halting so the next wedge is diagnosable from a CI log - document that an owner-context GraphicsLayer with alpha inside a native popup is supported (picture replay, no foreign GPU resource)
# Conflicts: # decorated-window-tao/src/test/kotlin/dev/nucleusframework/window/tao/ComposableTargetIsolationFixture.kt # nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedDialog.kt # nucleus-application/src/main/kotlin/dev/nucleusframework/application/DecoratedWindow.kt # nucleus-application/src/main/kotlin/dev/nucleusframework/application/NucleusWindowHost.kt
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.
inprogress