feat(tao): accept Compose 1.12 window API v2 - #634
Draft
kdroidFilter wants to merge 11 commits into
Draft
Conversation
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.
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.
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.
kdroidFilter
force-pushed
the
feat/compose-window-api-v2
branch
from
September 1, 2026 16:56
6ec51dc to
27fc2ef
Compare
… 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).
`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.
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.
Summary
Tao now accepts Compose Multiplatform 1.12's experimental window/dialog API v2 (
androidx.compose.ui.window.v2).DecoratedWindow/DecoratedDialog/HostedWindow/HostedDialogoverloads take v2WindowState/DialogState(no defaultstate, so existing v1 call sites stay unambiguous).requestBounds,requestPlacement,requestSize,requestMinimized) is applied asynchronously through the existing v1 window plumbing.bounds/placement/isMinimizedare published once the native window has been shown (isInitialized).minSize/maxSizeon the v2 overloads; max inner size is a new Tao JNI (set_max_inner_size).examples/tao-demouses v2rememberWindowState+requestPlacement.Internals that Compose keeps
internal(request channels,_bounds) are reached the same way asTaoTransferableAccess: a Java friend-package accessor inandroidx.compose.ui.window.v2.Known gaps:
requestScreenis drained but not applied (Tao only exposes the primary work area). CustomWindowBoundsProviderlambdas that need a live AWT window are evaluated via a dummy geometry peer.Test plan
ComposeWindowV2BridgeTest— default v2 state maps to 800×600 / PlatformDefault; initialized state copies observed boundsNucleusWindowHostTest— HostedWindow v2 forwards state/min/max to the ambient host:examples:tao-demo:runand toggle Floating / Maximized / Fullscreen