Skip to content

chore(tao): migrate decorated-window-tao to Compose 1.12.0-beta02 - #354

Draft
kdroidFilter wants to merge 1 commit into
mainfrom
chore/compose-1.12-beta02-tao
Draft

chore(tao): migrate decorated-window-tao to Compose 1.12.0-beta02#354
kdroidFilter wants to merge 1 commit into
mainfrom
chore/compose-1.12-beta02-tao

Conversation

@kdroidFilter

@kdroidFilter kdroidFilter commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Why

Get ahead of the Compose 1.12 release: the no-AWT decorated-window-tao
backend drives Compose's internal androidx.compose.ui.scene.* APIs directly,
so it's the one module that breaks on a minor Compose bump. This PR does that
migration against 1.12.0-beta02 so we're ready to flip to 1.12.0 stable the
day it ships (just move the catalog line).

Rebased on main (was conflicting: the tao JetBrains-style refactor moved
every touched file into scene/ / popup/ / deco/ / ffi/, so the
migration was re-applied on the new layout rather than replayed textually).
1.12.0 stable is not out yet — latest published is 1.12.0-beta02.

What changed in the 1.12 ComposeScene API

  • ComposeScene.render(canvas, nanoTime) removed → three steps:
    FrameRecomposer.performFrame(nanoTime) + ComposeScene.measureAndLayout() +
    ComposeScene.draw(canvas).
  • Scene factories (Canvas/PlatformLayersComposeScene) drop coroutineContext
    • invalidate; now take a FrameRecomposer (which owns the frame clock) +
      invalidateLayout / invalidateDraw. The manual per-host BroadcastFrameClock
      and frameClock.sendFrame() calls are gone.
  • ComposeSceneContext.createLayer: compositionContextconsumePointerInputOutside.
  • ComposeSceneLayer.setContent(parentCompositionContext, content) + new abstract
    consumePointerInputOutside property.

Approach

Scene create/render/dispose plumbing is centralized in TaoSceneBundle
(scene/TaoSceneBundle.kt). Each host/popup/overlay stores a TaoSceneBundle?
and keeps val scene get() = sceneBundle?.scene, so the ~20 scene?.… call
sites per file are untouched — only create/render/close move. Future Compose
bumps touch one file for the shared logic.

Behaviour is kept at parity with the 1.11 path: the paced frameNs is still fed
to the frame clock for standalone popups, and every invalidation
(invalidateLayout/invalidateDraw + the recomposer's own) funnels into the
same coalescing frame scheduler that invalidate used before.

The offscreen scene test harness (TaoSceneTestHarness, added after the first
revision of this PR) moves onto the same bundle: its explicit
frameClock.sendFrame + second pumpUntilIdle is now performed inside
recordSceneToPictureFrameRecomposer.performFrame, which flushes its own
dispatchers around the tick.

Note: SingleComposeSceneRenderingScope

Compose Desktop's AWT path wraps the three render calls in
SingleComposeSceneRenderingScope.render, but that method is Kotlin-internal
(JVM-public, module-private) and invisible to external backends. TaoSceneBundle
inlines performFramemeasureAndLayoutdraw directly. The only thing
lost vs. upstream is its isRendering-guarded invalidation de-dup — which the
old Tao code didn't have either, and the FrameDispatcher coalescing covers it.

Fallout: AGP 9 / Java 17

Compose 1.12 also pulls AndroidX Compose 1.12 into examples:cmp-demo's Android
target, which requires AGP ≥ 9.1 and compileSdk 37. That cascades:

  • agp 8.13.2 → 9.1.1 (9.2+ needs Gradle ≥ 9.4.1, we're on 9.4.0), cmp-demo
    compileSdk/targetSdk 35 → 37.
  • android.builtInKotlin=false + android.newDsl=false: since AGP 9,
    com.android.application is rejected alongside the KMP plugin, which points at
    com.android.kotlin.multiplatform.library — a library plugin, unusable for
    cmp-demo (a KMP module that builds a real Android app) without splitting it into
    a separate :androidApp module. These are AGP's documented escape hatches.
  • plugin-build/plugin jvmTarget 11 → 17 — AGP 9 artifacts are built for 17
    and can't be resolved by an 11-targeted consumer. User-visible: the Nucleus
    Gradle plugin now needs a Gradle daemon on JVM 17+ (mandatory since Gradle 9
    anyway; Gradle 8 + JVM 11 setups are no longer supported).
  • Dropped a stray org.jetbrains.skia.Surface import from cmp-demo's
    commonMain — unused, and unresolvable on Android. That target had never
    compiled; it does now (verified assembleDebug).

Verification

JBR 21, BUILD SUCCESSFUL, ktlint + detekt + apiCheck clean:

  • :decorated-window-tao:check78 tests pass, including the animation,
    pointer, popup, render and battery-drift suites that exercise the new frame
    path (--rerun-tasks).
  • assemble across the whole project, incl. :examples:cmp-demo:assembleDebug
    (Android), the jni / jbr / jewel / material2 / material3 backends,
    graalvm-runtime, nucleus-application, and the tao examples.
  • plugin-build: :plugin:check + :plugin:validatePlugins — 209/210 tests
    pass at jvmTarget 17.
  • The preMerge check set for every module listed in the preMerge task.

Two pre-existing local failures, both reproduced on unmodified main and
unrelated to this PR (both are environment-specific, not Compose-related):
:fs-watcher:test hangs on the real-filesystem test, and LinuxSignerTest
fails at gpg --gen-key.

Not done: native runtime smoke tests on macOS / Windows / Linux (headful
window/popup behaviour).

Before merge

  • Switch catalog compose line to 1.12.0 stable (not yet published)
  • Run the native standalone-popup smoke tests on the three OSes
  • Confirm the plugin's JVM 17 floor is acceptable as a released change

@kdroidFilter
kdroidFilter force-pushed the chore/compose-1.12-beta02-tao branch from f7870fd to 3799541 Compare July 26, 2026 11:44
Adapt the no-AWT Tao backend to the Compose 1.12 ComposeScene API changes so
it is ready for Compose 1.12 stable:

- ComposeScene.render(canvas, nanoTime) is gone -> FrameRecomposer.performFrame
  + ComposeScene.measureAndLayout + ComposeScene.draw
- scene factories take a FrameRecomposer + invalidateLayout/invalidateDraw
  instead of coroutineContext + invalidate; the FrameRecomposer owns the frame
  clock, so the manual per-host BroadcastFrameClock + sendFrame is removed
- ComposeSceneContext.createLayer: compositionContext -> consumePointerInputOutside
- ComposeSceneLayer.setContent(parentCompositionContext, content) + new
  consumePointerInputOutside property

The scene create + render + dispose plumbing is centralized in TaoSceneBundle
(scene/TaoSceneBundle.kt) so future Compose bumps touch one place. Each host,
popup layer and overlay keeps `val scene get() = sceneBundle?.scene`, leaving
its ~20 `scene?.…` call sites untouched. Behaviour is kept at parity with the
1.11 path (paced frameNs preserved for standalone popups, invalidations
funnelled to the same coalescing scheduler).

The offscreen scene test harness moves to the same bundle: its explicit
sendFrame + pump is now performed inside recordSceneToPicture. All 78
decorated-window-tao tests pass unchanged, including the animation, popup,
render and drift batteries.

Compose 1.12 also pulls AndroidX Compose 1.12 into examples:cmp-demo's Android
target, which requires AGP >= 9.1 and compileSdk 37:

- agp 8.13.2 -> 9.1.1, cmp-demo compileSdk/targetSdk 35 -> 37
- android.builtInKotlin=false / android.newDsl=false: AGP 9 refuses
  com.android.application next to the KMP plugin and points at the KMP
  *library* plugin, which cmp-demo (a KMP module that builds a real Android
  app) cannot use without being split into a separate :androidApp module
- plugin-build/plugin jvmTarget 11 -> 17: AGP 9 artifacts are built for 17 and
  are unresolvable from an 11-targeted consumer. Users now need a Gradle daemon
  on JVM 17+ (mandatory since Gradle 9 anyway)
- drop a stray `org.jetbrains.skia.Surface` import from cmp-demo's commonMain:
  unused, and unresolvable on Android — that target had never compiled
@kdroidFilter
kdroidFilter force-pushed the chore/compose-1.12-beta02-tao branch from 3799541 to 1bcd42f Compare July 26, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant