From 3395fd2f66b4a5724163497787fc96bbd1caf0a1 Mon Sep 17 00:00:00 2001 From: ApoloApps Date: Tue, 25 Aug 2026 15:44:15 +0200 Subject: [PATCH 1/6] Add common logic for supporting UiMediaScope in Skiko platforms --- .../ui/platform/CompositionLocals.skiko.kt | 4 ++ .../platform/DefaultHapticFeedback.skiko.kt | 26 ----------- .../ui/platform/PlatformContext.skiko.kt | 43 ++++++++++++++++++- 3 files changed, 46 insertions(+), 27 deletions(-) delete mode 100644 compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.skiko.kt diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt index 770d56dd94907..c8f948a0e217d 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalMediaQueryApi::class) package androidx.compose.ui.platform @@ -25,7 +26,9 @@ import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.LocalSaveableStateRegistry import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.LocalUiMediaScope import androidx.lifecycle.LifecycleOwner import androidx.savedstate.compose.LocalSavedStateRegistryOwner @@ -94,6 +97,7 @@ internal fun ProvidePlatformCompositionLocals( LocalPlatformScreenReader provides platformContext.screenReader, LocalPlatformWindowInsets provides platformContext.windowInsets, LocalPlatformPrefetchScheduler provides platformContext.prefetchScheduler, + LocalUiMediaScope provides platformContext.mediaEnvironment, androidx.lifecycle.compose.LocalLifecycleOwner provides platformContext.architectureComponentsOwner.lifecycleOwner, LocalSavedStateRegistryOwner provides platformContext.architectureComponentsOwner.savedStateRegistryOwner, LocalSaveableStateRegistry provides saveableStateRegistry, diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.skiko.kt deleted file mode 100644 index a0c91764e5190..0000000000000 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.skiko.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.ui.platform - -import androidx.compose.ui.hapticfeedback.HapticFeedback -import androidx.compose.ui.hapticfeedback.HapticFeedbackType - -// TODO(demin): implement HapticFeedback -internal object DefaultHapticFeedback : HapticFeedback { - override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) { - } -} \ No newline at end of file diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt index c39614b2b162e..de983f435efe5 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalMediaQueryApi::class) + package androidx.compose.ui.platform import androidx.compose.runtime.getValue @@ -20,13 +22,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.ComposeUiFlags import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.FrameRateCategory import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.UiMediaScope import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.InputMode import androidx.compose.ui.input.InputModeManager import androidx.compose.ui.input.pointer.PointerIcon @@ -45,6 +50,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeOptions import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.intl.LocaleList +import androidx.compose.ui.unit.Dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelStoreOwner @@ -156,7 +162,7 @@ interface PlatformContext { } val textToolbar: TextToolbar get() = EmptyTextToolbar - val hapticFeedback: HapticFeedback get() = DefaultHapticFeedback + val hapticFeedback: HapticFeedback get() = NoOpHapticFeedback fun setPointerIcon(pointerIcon: PointerIcon) = Unit val parentFocusManager: FocusManager get() = EmptyFocusManager @@ -217,6 +223,17 @@ interface PlatformContext { */ val prefetchScheduler: PlatformPrefetchScheduler get() = NoOpPlatformPrefetchScheduler + /** + * Media-related information exposed to the composition. + * + * This provides platform-specific environment details such as window posture, + * pointer precision, keyboard type, and device capabilities. The default + * implementation is a no-op environment that reports neutral values so code + * using media state remains safe on platforms that do not provide a richer + * implementation. + */ + val mediaEnvironment: UiMediaScope get() = NoOpMediaEnvironment + interface RootForTestListener { fun onRootForTestCreated(root: PlatformRootForTest) fun onRootForTestDisposed(root: PlatformRootForTest) @@ -389,3 +406,27 @@ internal class DelegateRootForTestListener : PlatformContext.RootForTestListener } } } + +private object NoOpHapticFeedback : HapticFeedback { + override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) { + } +} + +private object NoOpMediaEnvironment : UiMediaScope { + override val windowPosture: UiMediaScope.Posture + get() = UiMediaScope.Posture.Flat + override val windowWidth: Dp + get() = Dp.Unspecified + override val windowHeight: Dp + get() = Dp.Unspecified + override val pointerPrecision: UiMediaScope.PointerPrecision + get() = UiMediaScope.PointerPrecision.None + override val keyboardKind: UiMediaScope.KeyboardKind + get() = UiMediaScope.KeyboardKind.None + override val hasMicrophone: Boolean + get() = false + override val hasCamera: Boolean + get() = false + override val viewingDistance: UiMediaScope.ViewingDistance + get() = UiMediaScope.ViewingDistance.Near +} \ No newline at end of file From 963fc1771a958d8efac35915ef97640db2060c0a Mon Sep 17 00:00:00 2001 From: ApoloApps Date: Tue, 25 Aug 2026 16:46:28 +0200 Subject: [PATCH 2/6] Fix web tests failing after removing DefaultHapticFeedback in favor of a private NoOpHapticFeedback --- .../androidx/compose/ui/platform/DefaultHapticFeedback.web.kt | 4 +--- .../androidx/compose/ui/window/ComposeWindowInternal.web.kt | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.web.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.web.kt index 5dae6a1178b53..de75a82314b6f 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.web.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.web.kt @@ -39,8 +39,6 @@ internal class WebHapticFeedback : HapticFeedback { private val SoftTickVibrationPattern = vibrationPatternOf(6) private val LongPressVibrationPattern = vibrationPatternOf(0, 30) private val VirtualKeyVibrationPattern = vibrationPatternOf(0, 20) - - fun webHapticFeedbackOrDefault(): HapticFeedback = if (isVibrationSupported()) WebHapticFeedback() else DefaultHapticFeedback } override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) { @@ -81,7 +79,7 @@ private fun vibrate(pattern: JsArray) { js("window.navigator.vibrate(pattern)") } -private fun isVibrationSupported(): Boolean = js( +internal fun isVibrationSupported(): Boolean = js( //language=javascript """ typeof window !== 'undefined' && diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt index 1b62a170f55c5..0c6a39dcb6d2c 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt @@ -79,6 +79,7 @@ import androidx.compose.ui.platform.PlatformOutOfFrameExecutor import androidx.compose.ui.platform.PlatformPrefetchScheduler import androidx.compose.ui.platform.WebPrefetchScheduler import androidx.compose.ui.platform.isIdleCallbackSupported +import androidx.compose.ui.platform.isVibrationSupported import androidx.compose.ui.scene.ComposeSceneDragAndDropNode import androidx.compose.ui.scene.ComposeScenePointer import androidx.compose.ui.scene.PointerEventResult @@ -309,7 +310,7 @@ internal class ComposeWindow( } override val hapticFeedback by lazy(LazyThreadSafetyMode.NONE) { - WebHapticFeedback.webHapticFeedbackOrDefault() + if (isVibrationSupported()) WebHapticFeedback() else super.hapticFeedback } override val prefetchScheduler: PlatformPrefetchScheduler = From 696ee334e0cd51bdb1ea630537ce02287e9577d9 Mon Sep 17 00:00:00 2001 From: ApoloApps Date: Wed, 26 Aug 2026 14:30:43 +0200 Subject: [PATCH 3/6] Rename NoOpMediaEnvironment to EmptyMediaEnvironment --- .../androidx/compose/ui/platform/PlatformContext.skiko.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt index de983f435efe5..97ced6843e9ef 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt @@ -232,7 +232,7 @@ interface PlatformContext { * using media state remains safe on platforms that do not provide a richer * implementation. */ - val mediaEnvironment: UiMediaScope get() = NoOpMediaEnvironment + val mediaEnvironment: UiMediaScope get() = EmptyMediaEnvironment interface RootForTestListener { fun onRootForTestCreated(root: PlatformRootForTest) @@ -412,7 +412,7 @@ private object NoOpHapticFeedback : HapticFeedback { } } -private object NoOpMediaEnvironment : UiMediaScope { +private object EmptyMediaEnvironment : UiMediaScope { override val windowPosture: UiMediaScope.Posture get() = UiMediaScope.Posture.Flat override val windowWidth: Dp From 6ddfeed742e9c4d52a0cafb84ed7556534a6149a Mon Sep 17 00:00:00 2001 From: ApoloApps Date: Wed, 26 Aug 2026 18:34:32 +0200 Subject: [PATCH 4/6] Cleaner NoOpHapticFeedback --- .../androidx/compose/ui/platform/PlatformContext.skiko.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt index 97ced6843e9ef..5449973bda46d 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt @@ -408,8 +408,7 @@ internal class DelegateRootForTestListener : PlatformContext.RootForTestListener } private object NoOpHapticFeedback : HapticFeedback { - override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) { - } + override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) = Unit } private object EmptyMediaEnvironment : UiMediaScope { From ccce0154011b5cb677c7a4637531bc84efd05459 Mon Sep 17 00:00:00 2001 From: ApoloApps Date: Wed, 26 Aug 2026 19:37:34 +0200 Subject: [PATCH 5/6] Move OptIns as much closer to the point of usage as possible --- .../androidx/compose/ui/platform/CompositionLocals.skiko.kt | 3 +-- .../androidx/compose/ui/platform/PlatformContext.skiko.kt | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt index c8f948a0e217d..438a4a380a00d 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(ExperimentalMediaQueryApi::class) package androidx.compose.ui.platform @@ -69,7 +68,7 @@ val LocalPlatformPrefetchScheduler = staticCompositionLocalOf, diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt index 5449973bda46d..f0ba52b3b1bc6 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(ExperimentalMediaQueryApi::class) package androidx.compose.ui.platform @@ -232,6 +231,7 @@ interface PlatformContext { * using media state remains safe on platforms that do not provide a richer * implementation. */ + @OptIn(ExperimentalMediaQueryApi::class) val mediaEnvironment: UiMediaScope get() = EmptyMediaEnvironment interface RootForTestListener { @@ -411,6 +411,7 @@ private object NoOpHapticFeedback : HapticFeedback { override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) = Unit } +@OptIn(ExperimentalMediaQueryApi::class) private object EmptyMediaEnvironment : UiMediaScope { override val windowPosture: UiMediaScope.Posture get() = UiMediaScope.Posture.Flat From e2068ee90bb16ea3b25e0f7d625cbe14fcdb20be Mon Sep 17 00:00:00 2001 From: ApoloApps Date: Mon, 31 Aug 2026 13:32:04 +0200 Subject: [PATCH 6/6] Rename mediaEnvironment to mediaScope --- .../androidx/compose/ui/platform/CompositionLocals.skiko.kt | 3 +-- .../androidx/compose/ui/platform/PlatformContext.skiko.kt | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt index 438a4a380a00d..7eff73fd2255c 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt @@ -19,7 +19,6 @@ package androidx.compose.ui.platform import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.LocalHostDefaultProvider import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.remember @@ -96,7 +95,7 @@ internal fun ProvidePlatformCompositionLocals( LocalPlatformScreenReader provides platformContext.screenReader, LocalPlatformWindowInsets provides platformContext.windowInsets, LocalPlatformPrefetchScheduler provides platformContext.prefetchScheduler, - LocalUiMediaScope provides platformContext.mediaEnvironment, + LocalUiMediaScope provides platformContext.mediaScope, androidx.lifecycle.compose.LocalLifecycleOwner provides platformContext.architectureComponentsOwner.lifecycleOwner, LocalSavedStateRegistryOwner provides platformContext.architectureComponentsOwner.savedStateRegistryOwner, LocalSaveableStateRegistry provides saveableStateRegistry, diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt index f0ba52b3b1bc6..0db4ede955720 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt @@ -232,7 +232,7 @@ interface PlatformContext { * implementation. */ @OptIn(ExperimentalMediaQueryApi::class) - val mediaEnvironment: UiMediaScope get() = EmptyMediaEnvironment + val mediaScope: UiMediaScope get() = EmptyMediaScope interface RootForTestListener { fun onRootForTestCreated(root: PlatformRootForTest) @@ -412,7 +412,7 @@ private object NoOpHapticFeedback : HapticFeedback { } @OptIn(ExperimentalMediaQueryApi::class) -private object EmptyMediaEnvironment : UiMediaScope { +private object EmptyMediaScope : UiMediaScope { override val windowPosture: UiMediaScope.Posture get() = UiMediaScope.Posture.Flat override val windowWidth: Dp