From 3395fd2f66b4a5724163497787fc96bbd1caf0a1 Mon Sep 17 00:00:00 2001 From: ApoloApps Date: Tue, 25 Aug 2026 15:44:15 +0200 Subject: [PATCH 1/5] 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/5] 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 355cb49abaa29026ccf40a5d1aa3d97366bc0fee Mon Sep 17 00:00:00 2001 From: ApoloApps Date: Tue, 25 Aug 2026 19:33:19 +0200 Subject: [PATCH 3/5] Add UiMediaScope support to Desktop --- .../compose/ui/ProvideSystemTheme.desktop.kt | 1 - .../PlatformMediaEnvironment.desktop.kt | 54 +++++++++++++++++++ .../ui/scene/ComposeSceneMediator.desktop.kt | 9 ++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformMediaEnvironment.desktop.kt diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ProvideSystemTheme.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ProvideSystemTheme.desktop.kt index 59beedfd90722..a78eb61f31ef9 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ProvideSystemTheme.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ProvideSystemTheme.desktop.kt @@ -30,7 +30,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.jetbrains.skiko.hostOs private var subscriberCount = 0 private var pollingJob: Job? = null diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformMediaEnvironment.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformMediaEnvironment.desktop.kt new file mode 100644 index 0000000000000..15cddcfa3c768 --- /dev/null +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformMediaEnvironment.desktop.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2026 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. + */ + +@file:OptIn(ExperimentalMediaQueryApi::class) + +package androidx.compose.ui.platform + +import androidx.compose.ui.ExperimentalMediaQueryApi +import androidx.compose.ui.UiMediaScope +import androidx.compose.ui.unit.Dp + +internal class DesktopMediaEnvironment(val windowInfo: WindowInfo) : UiMediaScope { + + override val windowPosture: UiMediaScope.Posture + get() = UiMediaScope.Posture.Flat + + override val windowWidth: Dp + get() = windowInfo.containerDpSize.width + + override val windowHeight: Dp + get() = windowInfo.containerDpSize.height + + override val pointerPrecision: UiMediaScope.PointerPrecision + get() = UiMediaScope.PointerPrecision.Fine + + override val keyboardKind: UiMediaScope.KeyboardKind + get() = UiMediaScope.KeyboardKind.Physical + + override val hasMicrophone: Boolean + get() = true //inferred as always having at least 1 microphone + + override val hasCamera: Boolean + get() = false //no reliable way to get it + + override val viewingDistance: UiMediaScope.ViewingDistance + get() = UiMediaScope.ViewingDistance.Near + + fun dispose() { + // No-op for now, but can be used to clean up resources if needed in the future. + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt index 84274fe24c474..b1f3b50ba1b6c 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt @@ -21,7 +21,9 @@ import androidx.compose.runtime.CompositionLocalContext import androidx.compose.runtime.mutableStateSetOf import androidx.compose.ui.ComposeFeatureFlags import androidx.compose.ui.ComposeUiFlags +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.ProvideSystemTheme +import androidx.compose.ui.UiMediaScope import androidx.compose.ui.awt.AwtEventListener import androidx.compose.ui.awt.AwtEventListeners import androidx.compose.ui.awt.DebouncingEdtExecutor @@ -51,6 +53,7 @@ import androidx.compose.ui.navigationevent.BackNavigationEventInput import androidx.compose.ui.platform.AwtDragAndDropManager import androidx.compose.ui.platform.DefaultInputModeManager import androidx.compose.ui.platform.DelegateRootForTestListener +import androidx.compose.ui.platform.DesktopMediaEnvironment import androidx.compose.ui.platform.DesktopTextInputService import androidx.compose.ui.platform.DesktopTextInputService2 import androidx.compose.ui.platform.FrameRecomposer @@ -160,6 +163,8 @@ internal class ComposeSceneMediator( DesktopTextInputService(platformComponent) } + private val desktopMediaEnvironment = DesktopMediaEnvironment(windowInfo = windowContext.windowInfo) + private val textInputService2 by lazy(LazyThreadSafetyMode.NONE) { DesktopTextInputService2(platformComponent) } @@ -628,6 +633,8 @@ internal class ComposeSceneMediator( // Since rendering will not happen after, we need to execute all scheduled updates interopContainer.dispose() + desktopMediaEnvironment.dispose() + _onComponentAttached = null } @@ -833,6 +840,8 @@ internal class ComposeSceneMediator( private inner class DesktopPlatformContext : PlatformContext { override val windowInfo: WindowInfo get() = windowContext.windowInfo + @OptIn(ExperimentalMediaQueryApi::class) + override val mediaEnvironment: UiMediaScope get() = desktopMediaEnvironment override val architectureComponentsOwner get() = this@ComposeSceneMediator.architectureComponentsOwner override val isWindowTransparent: Boolean get() = windowContext.isWindowTransparent From 11d3f644866238bcdb5032595365daf53c578df1 Mon Sep 17 00:00:00 2001 From: Apolo Date: Tue, 1 Sep 2026 18:36:10 +0200 Subject: [PATCH 4/5] Discard changes to compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/CompositionLocals.skiko.kt --- .../androidx/compose/ui/platform/CompositionLocals.skiko.kt | 2 -- 1 file changed, 2 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 4029c37e85fee..0f0e953a810fe 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 @@ -106,7 +105,6 @@ 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, From 82b4964b5c617dcac62c1be618778f25249e4e50 Mon Sep 17 00:00:00 2001 From: ApoloApps Date: Tue, 1 Sep 2026 18:44:20 +0200 Subject: [PATCH 5/5] Renaming MediaEnvironment to MediaScope in Desktop and fixing after main PR merge --- ...mMediaEnvironment.desktop.kt => MediaScope.desktop.kt} | 2 +- .../compose/ui/scene/ComposeSceneMediator.desktop.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/{PlatformMediaEnvironment.desktop.kt => MediaScope.desktop.kt} (95%) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformMediaEnvironment.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/MediaScope.desktop.kt similarity index 95% rename from compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformMediaEnvironment.desktop.kt rename to compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/MediaScope.desktop.kt index 15cddcfa3c768..97b9033cc15b0 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/PlatformMediaEnvironment.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/MediaScope.desktop.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.UiMediaScope import androidx.compose.ui.unit.Dp -internal class DesktopMediaEnvironment(val windowInfo: WindowInfo) : UiMediaScope { +internal class DesktopMediaScope(val windowInfo: WindowInfo) : UiMediaScope { override val windowPosture: UiMediaScope.Posture get() = UiMediaScope.Posture.Flat diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt index 26b16639991be..61d4d3823ffbb 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt @@ -53,7 +53,7 @@ import androidx.compose.ui.navigationevent.BackNavigationEventInput import androidx.compose.ui.platform.AwtDragAndDropManager import androidx.compose.ui.platform.DefaultInputModeManager import androidx.compose.ui.platform.DelegateRootForTestListener -import androidx.compose.ui.platform.DesktopMediaEnvironment +import androidx.compose.ui.platform.DesktopMediaScope import androidx.compose.ui.platform.DesktopTextInputService import androidx.compose.ui.platform.DesktopTextInputService2 import androidx.compose.ui.platform.FrameRecomposer @@ -165,7 +165,7 @@ internal class ComposeSceneMediator( DesktopTextInputService(platformComponent) } - private val desktopMediaEnvironment = DesktopMediaEnvironment(windowInfo = windowContext.windowInfo) + private val desktopMediaScope = DesktopMediaScope(windowInfo = windowContext.windowInfo) private val textInputService2 by lazy(LazyThreadSafetyMode.NONE) { DesktopTextInputService2(platformComponent) @@ -635,7 +635,7 @@ internal class ComposeSceneMediator( // Since rendering will not happen after, we need to execute all scheduled updates interopContainer.dispose() - desktopMediaEnvironment.dispose() + desktopMediaScope.dispose() _onComponentAttached = null } @@ -843,7 +843,7 @@ internal class ComposeSceneMediator( private inner class DesktopPlatformContext : PlatformContext { override val windowInfo: WindowInfo get() = windowContext.windowInfo @OptIn(ExperimentalMediaQueryApi::class) - override val mediaEnvironment: UiMediaScope get() = desktopMediaEnvironment + override val mediaScope: UiMediaScope get() = desktopMediaScope override val taskDispatchers: TaskDispatchers = object : TaskDispatchers { override val Default = Dispatchers.Default