Introduces several caching of objects across graphics Skiko code - #3117
Introduces several caching of objects across graphics Skiko code#3117Apolo (ApoloApps) wants to merge 121 commits into
Conversation
… This aligns to Android's behaviour
…for indexed loop instead of generic Collection which allocates the iterator)
…ct on each frame (it reuses the same via CanvasHolder.drawInto). This aligns the behaviour with Android's. Uses the exact same pattern
…lections to avoid boxing, iterators and improve performance. (Similar change to PointerToPositionMap)
Ivan Matkov (MatkovIvan)
left a comment
There was a problem hiding this comment.
Thanks!
My main concern here is the shape of the newly introduced public API - it doesn't seem that it should be exposed like that for wide audience with full compatibility guarantees
| * Holder class that is used to issue scoped calls to a [Canvas] from the framework equivalent | ||
| * canvas without having to allocate an object on each draw call | ||
| */ | ||
| class CanvasHolder { |
There was a problem hiding this comment.
Note: a new public-stable API that we'll have to maintain forever
| internal val internalSkiaCanvas: SkCanvas, | ||
| ) : Canvas { | ||
| @PublishedApi | ||
| internal class SkiaBackedCanvas : Canvas { |
There was a problem hiding this comment.
It wasn't supposed to be exposed. Again, requirement for binary compatibility in a place where we're not ready to do it
|
I'll wait for the #3126 changes |
|
Separated non-graphics related changes to this PR #3164 to unblock merging some parts of this PR. After other PR merged I will be rebasing (as the other PR contains commits of this one) |
Separated the changes from #3117 that don't involve graphics (which are currently blocked until separation of Skiko from Compose Ui and migration to Runtime Registration) as to unblock them from merging into main This includes changes SyntheticEventSender and more minor changes ## Release Notes N/A
Align implementation with the `SetComposingRegionCommand`. Fixes https://youtrack.jetbrains.com/issue/CMP-10281 ## Release Notes ### Fixes - Multiple Platforms - Fix crash in method setComposingRegion when calling it with inverted or invalid region
## Issues Fixed Fixes: [CMP-10293](https://youtrack.jetbrains.com/issue/CMP-10293) Update GitHub Actions to use newer iOS simulators for compose tests. Action were changed to use 'iPhone 17' (iOS 26.5), 'iPad Pro 11-inch (M5), Xcode_26.5, macos-26-xlarge ## Release Notes N/A --------- Co-authored-by: Andrei Salavei <andrei.salavei@jetbrains.com>
Make the ViewModelStore lifetime extended to the lifetime of the corresponding Compose Container. Fixes https://youtrack.jetbrains.com/issue/CMP-10175/iOS-ViewModel.onCleared-never-called-when-ComposeUIViewController-is-popped Fixes https://youtrack.jetbrains.com/issue/CMP-10159/Lifecycle-is-stuck-in-RESUMED ## Release Notes ### Fixes - iOS - `ViewModel` now receives `onCleared` call when Compose Container is deallocated.
The issue occurred because the `[_availableDrawables addObject:_lastPresentedDrawable];` was called too early that allowed to reuse the buffer before or during the presentation. The fix moves the releasing logic closer to the point of the buffer usage. Test: Benchmarks did not reveal any performance degradation. Fixes https://youtrack.jetbrains.com/issue/CMP-10208/Compose-may-change-frames-order-under-load ## Release Notes ### Fixes - iOS - Fix incorrect frames order during high load rendering
Fixes [CMP-10326](https://youtrack.jetbrains.com/issue/CMP-10326) Run Compose iOS instrumented tests on both iOS 18 and iOS 26 in GitHub Actions ## Release Notes N/A
…etBrains#3127) [CMP-10333](https://youtrack.jetbrains.com/issue/CMP-10333) DeprecationLevel.ERROR for skiko-specific types in ui-graphics API It touches common in fork to move deprecation level in 1.12. This part will be synchronized back during 1.13 release cycle Also, old desktop-only deprecations changed to `DeprecationLevel.HIDDEN` (no release notes as it's deprecated since 1.5 and there are no known usages). ## Release Notes ### Migration Notes - Multiple Platforms - Deprecation level of `NativeCanvas`, `NativePaint` typealiases and related methods has been changed to `ERROR`
…ns#3129) Describe proposed changes and the issue being fixed (Optional) Fixes [CMP-10292](https://youtrack.jetbrains.com/issue/CMP-10292) [iOS] Add instrumented tests for old and new edit menus with TextField and TextField2 ## Testing (Optional) Describe how you tested your changes (provide a snippet or/and steps) ## Release Notes N/A
## Summary
The non-Android frame pipeline accumulated several workarounds around
snapshot apply, owner invalidation, that diverge from what
`AndroidComposeView` / `Choreographer` actually do. This PR removes
those workarounds and aligns the pipeline to Android's canonical model.
Reference model (Android): `Choreographer.doFrame()` runs the whole
sequence recompose → layout → draw
without yielding to the `Looper`: no queued task (including
`GlobalSnapshotManager`'s apply post) runs between phases. Compose
Multiplatform was applying snapshots between phases and routing owner
invalidations through per-scene async queues - neither has an Android
counterpart.
## What changes
The branch is structured as reviewable commits:
1. **Replace per-scene `SnapshotInvalidationTracker` with inline
compose-thread invalidation**
Deletes `SnapshotInvalidationTracker` + its `CommandList` queue.
Owner snapshot observers now run inline when already on the compose
thread, otherwise post to the host's shared trampoline
(`FrameRecomposer.runOnComposeThread`), mirroring `AndroidComposeView`.
Removes the off-thread queues whose were the root of the CMP-7838 /
CMP-7067 deadlocks;
`useInUiThread {}` reverts to `use {}`.
2. **Align scene snapshot-apply to Android; tidy `FrameRecomposer` queue
naming**
`postponeInvalidation` no longer applies between measure/layout and
draw.
`draw()` advances the snapshot via `Snapshot.notifyObjectsInitialized()`
right before drawing - mirroring
`AndroidComposeView.dispatchDraw`, lighter than `sendApplyNotifications`
and coalescing a placement-write cascade into one frame.
Renames the two `FrameRecomposer` queues to match `AndroidUiDispatcher`
(`trampolineDispatcher`/`frameDispatcher`).
## Fixes
- [CMP-10287](https://youtrack.jetbrains.com/issue/CMP-10287) Per-scene
owner invalidation queue deviates from the Android model
## Known follow-up
[CMP-10291](https://youtrack.jetbrains.com/issue/CMP-10291)
`OffsetToFocusedRect` relies on inter-phase `Snapshot` apply between
layout and draw
## Release Notes
N/A
…supporting improved wheel event handling (JetBrains#3123) Web: Enhance scroll behavior by distinguishing trackpad gestures and supporting improved wheel event handling - Added precise trackpad gesture detection via heuristic methods. - Enhanced scroll calculation logic considering bounds and delta modes. Fixes https://youtrack.jetbrains.com/issue/CMP-10297 ## Testing Added ui tests ## Release Notes ### Fixes - Web - Added precise trackpad gesture detection via heuristic methods.
Removed caret snapping inside grapheme clusters hack because of moving this logic to our Skia fork This should be merged after merging [this PR to Skia](JetBrains/skia#18), and merging updated Skia to the compose-multiplatform-core. Technically, this is a revert of [this PR](JetBrains#2147) excluding the tests. Fixes: [CMP-8324 Move caret adjustments in complex glyphs fix to Skia](https://youtrack.jetbrains.com/issue/CMP-8324) ## Testing Autotests: `SkikoParagraphTest.getOffsetForPosition_insideComplexCharacter_shouldJumpToEnd` `SkikoParagraphTest.getOffsetForPosition_endOfLineWithComplexCharacter_shouldPositionCorrectly` This should be tested by QA. Test case [is here](https://youtrack.jetbrains.com/issue/CMP-8054/) ## Release Notes N/A
Part of [SKIKO-1100](https://youtrack.jetbrains.com/issue/SKIKO-1100) ## Release Notes N/A
…etBrains#3116) Handles the iOS 26 full-width `UINavigationController.contentSwipe` gesture recognizer, which is backed by a private `UIScreenEdgePanGestureRecognizer` subclass and can conflict with horizontal Compose scrollables such as `HorizontalPager`. Fixes https://youtrack.jetbrains.com/issue/CMP-9869 ## Testing Adds UIKitNavigationContentSwipeTest test suite covering that swiping over `HorizontalPager` does not pop the controller while swiping outside the pager still pops it. ## Release Notes ### Fixes - iOS - Fix swipe-back gesture conflict with horizontally scrollable components like `HorizontalPager`.
Fixes https://youtrack.jetbrains.com/issue/CMP-10406/iOS-EXCBADACCESS-crash-in-AccessibilityElement.accessibilityTraits-when-AX-queries-drag-source-descriptors ## Release Notes ### Fixes - iOS - Fix rare crash that occurs when iOS accesses a disposed AccessibilityElement.
Fixes `CAFrameRateRangeDefault` unavailable on iOS 14 and causing `symbol '_CAFrameRateRangeDefault' not found` crash on startup Fixes [CMP-10463](https://youtrack.jetbrains.com/issue/CMP-10463) [iOS] iOS 14. App crashes on startup ## Testing This should be tested by QA ## Release Notes ### Fixes - iOS - Fix crash on iOS 14 caused by referencing `CAFrameRateRangeDefault`, which is only available on iOS 15+
Added support for system window insets on Web. Compose scenes can now render edge-to-edge while respecting the safe area and the keyboard, exposing everything through the standard `WindowInsets` APIs (`WindowInsets.safeDrawing`, `WindowInsets.ime`, etc.). https://github.com/user-attachments/assets/b0a60f85-90ab-438c-9857-f0c15627f5f9 ### New experimental API A new configuration flag on `ComposeViewportConfiguration`: ```kotlin ComposeViewport(configure = { enableBrowserWindowInsets = true }) { // content that reacts to WindowInsets.safeDrawing, WindowInsets.ime, ... } ``` - Defaults to false, so existing behavior is unchanged. When enabled, the scene reads safe area insets and tracks IME geometry, and reports them through the standard WindowInsets APIs. - Requires the page to opt in to edge-to-edge rendering via the viewport meta tag: ``` <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> ``` Without `viewport-fit=cover` the browser applies safe area padding automatically and all `env(safe-area-inset-*)` variables return `0px`, so insets would always be zero. If the flag is enabled but the meta tag is missing, a warning is logged to the console. Fixes https://youtrack.jetbrains.com/issue/CMP-10141 ## Testing Added new web tests ## Release Notes ### Features - Web - Added experimental support for system window insets on Web.
…ains#3198) Fixes [CMP-9175](https://youtrack.jetbrains.com/issue/CMP-9175) Introduce a single desktop dependency for all platforms A new universal Desktop publication (`org.jetbrains.compose.desktop:desktop-jvm-all`) is now available. When packaging a desktop application, the compose gradle plugin resolves it to the appropriate platform-specific Skiko runtime for the target OS and architecture. ## Release Notes N/A
Uses https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask with user-blocking (current frame drawing is not blocked, but it must run after it as per KDoc) option to follow OutOfFrameExecutor contract. Tracing correctly shows this behaviour with very good performance. This takes precedence over idle periods for LazyLayouts prefetch schedulers (also as per KDoc). [PR for the latter](JetBrains#2928) Works in every browser except Apple ones (Webkit Mac & IOS, similar to requestIdleCallback support) but it is a question of 'when it will be implemented' cause they have a positive position on the standard but, as always, they lag behind on implementing these useful APIs Fixes [CMP-10273](https://youtrack.jetbrains.com/issue/CMP-10273/Support-OutOfFrameExecutor-on-Web) <img width="875" height="334" alt="webFrameLifecycle" src="https://github.com/user-attachments/assets/3c9a0803-34b2-401f-a78f-d33ea7d934fa" /> ## Release Notes ### Features - Web - Added support to LazyLayouts to run some work without blocking current frame's painting
This fixes iOS `BasicTextField` focus handoff when moving focus between separate Compose scenes, for example an outer `ComposeUIViewController` and a nested `ComposeUIView`. The fix adds a shared UIKit text input focus coordinator that releases the previously focused Compose scene before a different scene starts text input. Fixes https://youtrack.jetbrains.com/issue/CMP-10454/iOS-BasicTextField-focus-is-not-released-when-moving-between-nested-ComposeUIView-scenes ## Testing - Added `NestedComposeTextFieldFocusTest.focusMovesToTextFieldInNestedComposeUIView` - Verified the regression test fails before the fix: `Nested text field should take focus and release the outer text field` - Verified the same test passes after the fix: `Executed 1 test, with 0 failures` ## Release Notes ### Fixes - iOS - Fix `BasicTextField` focus handoff between nested Compose iOS scenes. ## Google CLA Signed or will sign the Google Contributor License Agreement.
## Summary - The `div#cmp_a11y_root` element was created with `position: absolute; top: 0; left: 0` but never given `width` or `height`, resulting in 0×0 dimensions - This makes the entire Compose content invisible to hit-test-based accessibility tools (Apple Accessibility Inspector, Appium), while VoiceOver works because it traverses the DOM sequentially - Sizes the a11y container via CSS (`width: 100%; height: 100%`) once at construction so it tracks the canvas through layout instead of paying a per-resize wasm2js bridge cost. Before fix on WebKit Browser: https://github.com/user-attachments/assets/95a99be9-71ce-4060-b08d-dbc201407084 After fix on WebKit Browser: https://github.com/user-attachments/assets/b67a7911-4cdc-42e4-a5a7-b0bf753609c3 https://github.com/user-attachments/assets/19aa9346-2d5a-4377-90b9-b6f86377a856 _iOS26 iPhone 15 Pro Max_ Fixes https://youtrack.jetbrains.com/issue/CMP-10172 ## Testing Tested manually with the `mpp/demo` `wasmJs` target, served via `wasmJsBrowserDevelopmentRun` and accessed from an iPhone 15 Pro Max (iOS 26) WebKit browser over LAN. Steps: 1. `./gradlew :mpp:demo:wasmJsBrowserDevelopmentRun` and load the demo. 2. In Safari Web Inspector / Accessibility Inspector, observe the dimensions and hit-testability of `div#cmp_a11y_root`. 3. Resize the viewport (rotate device, change window size) and confirm `cmp_a11y_root` stays in sync with the underlying `<canvas>`. Results: - **Before:** `cmp_a11y_root` is `0×0`; Accessibility Inspector hit-tests miss every Compose semantic node. VoiceOver still works (tree traversal). - **After:** `cmp_a11y_root` fills its parent and matches the canvas dimensions; hit-test-based tools (Accessibility Inspector, Appium) can reach every semantic node. VoiceOver behavior is unchanged. Because the container is sized with CSS `width: 100%; height: 100%` at construction, it tracks the canvas through layout automatically and stays in sync on resize with no per-resize bridge call. This should be tested by QA on: - Web/WASM Compose targets across browsers (Chromium, WebKit, Firefox). - Accessibility tooling: Accessibility Inspector, Appium, VoiceOver (regression check). ## Release Notes ### Fixes - Web - Fix `div#cmp_a11y_root` having 0×0 dimensions on Compose for Web (Kotlin/WASM), which made Compose content invisible to hit-test-based accessibility tools such as Accessibility Inspector and Appium. The a11y container is now sized to match the canvas and stays in sync on resize.
…L scrollable container (JetBrains#3177) Fixes https://youtrack.jetbrains.com/issue/CMP-10351 Other changes: - changed the touchSlop from 18.dp to 8.dp to align with mobile platforms ## Testing Added 2 demos with embedded ComposeViewport. The new demos available when passing an additional `demo` url parameter: - http://localhost:8080/?demo=embeddedWithScroll - http://localhost:8080/?demo=embedded Since the regression affect mobile web browsers, it makes sense to test in mobile. ## Release Notes ### Fixes - Web - Fix a regression in scrolling when ComposeViewport is embedded in HTML scrollable container
Fixes a regression in touch handling while preventing Compose from processing touches that should belong to the UIKit back-swipe gesture. Previously, if the back gesture recognizer was only tracking touches in `Possible`, `TouchesGestureRecognizer` could stop forwarding the touch to Compose too early. That fixed the drawer opening during back swipe conflict (JetBrains#3165 ), but it also caused simple taps near the screen edge to be dropped. Fixes [CMP-10465](https://youtrack.jetbrains.com/issue/CMP-10465) [iOS] Back button doesn't always respond to taps ## Testing Adds `ScreenEdgeTapTest` test suite This should be tested by QA ## Release Notes ### Fixes - iOS - _(prerelease fix)_ Fix an issue where taps near the screen edge could be missed when the back-swipe gesture recognizer was tracking touches
…JetBrains#3222) Fixes https://youtrack.jetbrains.com/issue/CMP-10270/Improvement-Web-App-should-fail-gracefully-when-WebGL-is-unavailable **Demo:** <img width="500" alt="Screenshot 2026-07-13 at 17 49 48" src="https://github.com/user-attachments/assets/f8aa7f51-fe35-4e7c-a78d-369f7cfd6c37" /> ## Testing Manual testing. Requires disabling hardware acceleration in the browser settings. See the YT ticket for details. ## Release Notes ### Fixes - Web - Show a meaningful message instead of an obscure app crash when the browser or device doesn't support WebGL2
…spans (JetBrains#3167) The goal of this PR is to use as backing DOM entities divs and spans (rather than textareas and inputs) Currently implemented logic of input processing in Compose Web conceptually is following: **Don't process keyboard typed event** We do not process (and by processing we mean sending keyboard event to the compose scene via scene.sendKeyEvent(keyEvent)) any keyboard event that we considered to be a so-called typed event, that is, an event that in a html context will lead for *modification of the text* for introducing new symbols **Don't process keyboard in composite mode (in a wide sense)** We do not process any keyboard event that we do believe happened when composite input or, say, accent dialogue is open. All modern browsers simply lacks the API that will help us to control or at leas monitor events inside such dialogues, we don't even know for sure whether they are opened or not **Each compose input has a dom counterpart on which we are listening keyboard events** It's very important nuance: we can not delegate events to the native DOM element, in order to process events in such element we are supposed to be focused (in a browser document sense, not in Compose sense) in the DOM element. We call such element backing input node We never sync state of backing input node in the direction of the state of corresponding Compose Input directly Insted we rely on bunch of js beforeinput events (as of now, "deleteContentBackward", "deleteWordBackward", "insertReplacementText", "insertText" and "insertCompositionText" to be precise) from wich we are deducing what Compose Edit commands should be created in order to be send to the Compose scene. We trying to deduce how exactly the backing input node was affected to resolve relevant params we need to pass to the edit commands We always sync selection and text state from Compose input to the backing DOM field In both cases there are guards that prevent us from resetting text and selection states when they are already equivalent and thus, synced. **Problem we have with this approach** As long as we've introduced this approach, the major source of bugs and issues we've encountered was related to deducing the correct params from the input events. The thing is that input events (in most cases) in theory contain all the information that we need to mimic it precisely, that is, create Edit Commands that we will send to the scene and, after being applied, will lead to syncing ot the texts states. In textareas and inputs, however, one very important part of such information is missing - we don't now the range of text currently affected byt this changes. Examples * when insertText or insertReplacementText happens, targetRange contains information on what text should be replaced, in current approach we just trying to guess what is happening based on what is the current selection and whether we, for instance, pressed Backspace * when deleteContentBackward or deleteWordBackward is happening we compute the borders of the words via standard Compose methods, relying on the fact that our current cursor position is correct. **What is the problem with such approach** There's a practical problem with this approach that we need to take into consideration and retest thoroughly inputs in multiple brtowsers and on multiple devices after any such fix. But, most importantly, this will be never enough - what we've learned from mobile devices that there can be a lot of very special cases when particular input events happens (and affects the actual output) but we just don't know. **What this PR introduces** * we are moving from input and textarea backing input fields to span and div accordingly * just like we don't process typed events, we are not processing Backspace event as well ## Testing manual + `./gradlew testWeb` ## Release Notes N/A
Fixes [CMP-9953](https://youtrack.jetbrains.com/issue/CMP-9953) Support Swipe Back gesture tests ## Testing - `DialogSwipeBackTest` - `PopupSwipeBackTest` - `HorizontalScrollSwipeBackTest` - `SwipeBackTest` ## Release Notes N/A
Changes: - remove `<h1>` to make CompsoeViewport fill in the entire viewport - add a small text label at the top right corner specifying the current kotlin target (js or wasm) - add a loading indicator (svg) - replaced `overflow: hidden` by `overflow-x: hidden`, so browser's pull-to-refresh works now <img width="313" height="115" alt="Screenshot 2026-07-14 at 11 26 54" src="https://github.com/user-attachments/assets/001a18c4-c899-4d8d-8a06-aafac204c8e1" /> ## Testing Manual. Only demo is affected ## Release Notes N/A
…tBrains#3227) `viewDidAppear` is sometimes triggered too late that lead to visible issues when loading Compose content Fixes https://youtrack.jetbrains.com/issue/CMP-10078/Compose-view-sometimes-not-rendered-in-LazyVStack ## Release Notes N/A
…ins#3224) Fixes [CMP-5702](https://youtrack.jetbrains.com/issue/CMP-5702) iOS. Floating cursor stop working horizontally after very quick swipe ## Testing Before: https://github.com/user-attachments/assets/52a32e2b-d368-401e-b77e-7682c7ce3490 After: https://github.com/user-attachments/assets/3fa6d2dc-1083-4fdd-9de3-4df912f795e2 ## Release Notes ### Fixes - iOS - Fixed the floating cursor no longer responding horizontally after a very quick swipe on iOS --------- Co-authored-by: Vladimir Mazunin <vladimir.mazunin@jetbrains.com>
…ng pointer events (JetBrains#3232) This PR rests on the specification of pointer events -> touch events relationship. Calling `preventDefault` on pointer events doesn't prevent the touch events. Unless we preventDefault the touch events when it's necessary (according to Compose events handling) the browser will handle the gestures or even manipulate the focus. Such focus changes lead to unexpected software keyboard behaviour in mobile browsers. This PR introduces conditional preventDefault in touchend event handler (more info in the code comments). We already had preventDefault in touchmove handler. Fixes https://youtrack.jetbrains.com/issue/CMP-10438/Web-mobile-Android.-Software-keyboard-wont-open-after-a-context-menu Updates the fix for https://youtrack.jetbrains.com/issue/CMP-10079/Web-Mobile.-iOS-26.4-only.-The-virtual-keyboard-jumps-after-each-tap ## Testing Added new tests simulating the browser's events sequence. Tested manually on Android Chrome and iOS Safary 26.5 This should be tested by QA ## Release Notes ### Fixes - Web - Fixed the hidden software keyboard in mobile browsers in focused text fields
…ains#3234) This PR updates skiko to the newest alpha version and updates demo project to use separate skiko-skottie dependencies ## Release Notes N/A
…listener (JetBrains#3231) The goal of this PR is to use the same code for listening media query events in both cases we are actually doing it: - in theme listener - in resize listener Apart form the fact that reusing is beneficial, code that were used in theme listener was actually working in a broader set of browsers. Also, this is preparation for having better approach for dispose. Demo was updated to support theming (see screenshot) <img width="2944" height="1840" alt="Screenshot_20260715-103828" src="https://github.com/user-attachments/assets/e1c155ae-2625-46a7-b4f4-a54e46a47191" /> <img width="2944" height="1840" alt="Screenshot_20260715-103859" src="https://github.com/user-attachments/assets/57c18bfa-756e-4355-a0da-c1be98fc53de" /> ## Testing manual ## Release Notes N/A
…the tests (JetBrains#3237) This change should reduce tests flakiness. ## Release Notes N/A
[CMP-10442](https://youtrack.jetbrains.com/issue/CMP-10442) Remove `ComposeUiFlags.useLegacyRenderNodeLayers` flag ## Release Notes ### Migration Notes - Multiple Platforms - Removed `ComposeUiFlags.useLegacyRenderNodeLayers` flag used to revert legacy (pre 1.8) render node layers for cases without `androidx.compose.ui.graphics.layer.GraphicsLayer`.
Separated the changes from JetBrains#3117 that don't involve graphics (which are currently blocked until separation of Skiko from Compose Ui and migration to Runtime Registration) as to unblock them from merging into main This includes changes SyntheticEventSender and more minor changes ## Release Notes N/A
…ct on each frame (it reuses the same via CanvasHolder.drawInto). This aligns the behaviour with Android's. Uses the exact same pattern
|
messed up the rebase, i'll close this one and start all over again cause it will be easier |
Fixes https://youtrack.jetbrains.com/issue/CMP-9463/Web-asComposeCanvas-method-returns-object-allocated-on-each-rendered-frame (for all targets, not only web)
Release Notes
N/A