Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,25 @@
* 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
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -81,7 +79,7 @@ private fun vibrate(pattern: JsArray<JsNumber>) {
js("window.navigator.vibrate(pattern)")
}

private fun isVibrationSupported(): Boolean = js(
internal fun isVibrationSupported(): Boolean = js(
//language=javascript
"""
typeof window !== 'undefined' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down