Skip to content
Closed
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e257b5b
Added support for UiMediaScope by creating PlatformMediaEnvironment, …
ApoloApps Jul 11, 2026
0836ca6
Forgotten imports for PlatformMediaEnvironment
ApoloApps Jul 11, 2026
66fbb7d
Added WebMediaEnvironment (support UiMediaScope in Web platforms)
ApoloApps Jul 11, 2026
c71d088
Delete KeyboardModeState as it is no longer used
ApoloApps Jul 11, 2026
3ed0607
Move Web ComposeWindowState to its own file for better separation of …
ApoloApps Jul 11, 2026
1038d75
Remove unused overloads in internal window file
ApoloApps Jul 11, 2026
26b7852
Cleanup initialization for PlatformContext so that it is only initial…
ApoloApps Jul 11, 2026
299b261
Move DefaultHapticFeedback to PlatformContext to follow the same patt…
ApoloApps Jul 11, 2026
cf14048
Achieve lazy ClipEventsTarget in a better way. Use providesComputed a…
ApoloApps Jul 11, 2026
eacfe25
Todo in WebMediaEnvironment
ApoloApps Jul 11, 2026
5d5e412
Cleanup imports
ApoloApps Jul 11, 2026
ac083db
Move variable to its correspondant editor fold
ApoloApps Jul 11, 2026
b302548
Web Test fixes
ApoloApps Jul 12, 2026
74c8d2d
Web Test fixes (Initialization error, userAgent was not init)
ApoloApps Jul 12, 2026
5a6e89a
Added UiMediaScope support for Jvm/Desktop target
ApoloApps Jul 12, 2026
c0b24b2
Added Dispose method in UiMediaScope support for Jvm/Desktop target
ApoloApps Jul 12, 2026
2900f74
Added support for UiMediaScope for iOS platform
ApoloApps Jul 12, 2026
73f45fe
Merge branch 'jb-main' into uiMediaSupport
ApoloApps Jul 13, 2026
64e79d7
Update WebWindowInsetsManager to make use of WebMediaEnvironment. Den…
ApoloApps Jul 13, 2026
05a4de0
Remove redundant UIWindow.density overload
ApoloApps Jul 13, 2026
a346f36
Rename IosMediaEnvironment to MediaEnvironment
ApoloApps Jul 13, 2026
d4271e1
Removed getViewWindowScene in favour of local uiWindow variable that …
ApoloApps Jul 13, 2026
6157886
Removed/Renamed dispose and initialization on iOS and remove it from …
ApoloApps Jul 13, 2026
e593bde
Use OptIn in place
ApoloApps Jul 13, 2026
bc757f3
Touches can come from Stylus in iOS platform, if any pointer is Stylu…
ApoloApps Jul 13, 2026
cf702f5
Added CaptureDeviceAvailability support to iOS MediaEnvironment by ad…
ApoloApps Jul 13, 2026
5a6f580
Merge branch 'jb-main' into uiMediaSupport
ApoloApps Jul 14, 2026
62c0a8e
Merge branch 'JetBrains:jb-main' into uiMediaSupport
ApoloApps Jul 14, 2026
10c8213
rename file to MediaEnvironment.ios.kt as well
ApoloApps Jul 14, 2026
a3b2c12
Fixes tests by adding a removed import that was necessary in ComposeS…
ApoloApps Jul 14, 2026
599abc5
Make MediaEnvironment implement KeyboardVisibilityObserver and add it…
ApoloApps Jul 14, 2026
d09a783
Web: Remove withoutReadObservation where not needed
ApoloApps Jul 15, 2026
7bf3b49
Web: Revert LocalActiveClipEventsTarget changes
ApoloApps Jul 15, 2026
abdb25a
Web: Fix WebDragAndDropManager logic by passing a Density provider (f…
ApoloApps Jul 15, 2026
108352e
Web: Defend WebWindowInsetsManager againts same density setter value
ApoloApps Jul 15, 2026
a8322fa
Remove todo from HapticFeedback
ApoloApps Jul 16, 2026
afc0aa4
Merge branch 'jb-main' into uiMediaSupport
ApoloApps Jul 28, 2026
936e9f5
Revert DesktopMediaEnvironment changes involving SystemTheme. Return …
ApoloApps Jul 28, 2026
f293351
Remove PlatformMediaEnvironment and its implementations and use UiMed…
ApoloApps Jul 28, 2026
36b842a
Rewrite MediaQueries with newly introduced MediaQueryListener in Web …
ApoloApps Jul 28, 2026
e7781af
Dump api files
ApoloApps Jul 29, 2026
0a4af71
Merge branch 'jb-main' into uiMediaSupport
ApoloApps Jul 29, 2026
303745e
Fix missing comma after iOS rebased changes
ApoloApps Jul 29, 2026
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 @@ -36,7 +36,7 @@ internal actual inline fun rememberClipboardEventsHandler(
isEnabled: Boolean,
): Boolean {
if (isEnabled) {
val clipEventsTargetProvider = LocalActiveClipEventsTarget.current
val clipEventsTarget = LocalActiveClipEventsTarget.current
DisposableEffect(Unit) {
val copyListener = EventListener { event ->
val textToCopy = onCopy()
Expand All @@ -62,15 +62,14 @@ internal actual inline fun rememberClipboardEventsHandler(
}
}

val eventsTarget = clipEventsTargetProvider()
eventsTarget?.addEventListener("copy", copyListener)
eventsTarget?.addEventListener("paste", pasteListener)
eventsTarget?.addEventListener("cut", cutListener)
clipEventsTarget?.addEventListener("copy", copyListener)
clipEventsTarget?.addEventListener("paste", pasteListener)
clipEventsTarget?.addEventListener("cut", cutListener)

onDispose {
eventsTarget?.removeEventListener("copy", copyListener)
eventsTarget?.removeEventListener("paste", pasteListener)
eventsTarget?.removeEventListener("cut", cutListener)
clipEventsTarget?.removeEventListener("copy", copyListener)
clipEventsTarget?.removeEventListener("paste", pasteListener)
clipEventsTarget?.removeEventListener("cut", cutListener)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal actual inline fun rememberClipboardEventsHandler(
isEnabled: Boolean,
): Boolean {
if (isEnabled) {
val clipEventsTargetProvider = LocalActiveClipEventsTarget.current
val clipEventsTarget = LocalActiveClipEventsTarget.current
DisposableEffect(Unit) {
val copyListener = EventListener { event ->
val textToCopy = onCopy()
Expand All @@ -63,15 +63,14 @@ internal actual inline fun rememberClipboardEventsHandler(
}
}

val eventsTarget = clipEventsTargetProvider()
eventsTarget?.addEventListener("copy", copyListener)
eventsTarget?.addEventListener("paste", pasteListener)
eventsTarget?.addEventListener("cut", cutListener)
clipEventsTarget?.addEventListener("copy", copyListener)
clipEventsTarget?.addEventListener("paste", pasteListener)
clipEventsTarget?.addEventListener("cut", cutListener)

onDispose {
eventsTarget?.removeEventListener("copy", copyListener)
eventsTarget?.removeEventListener("paste", pasteListener)
eventsTarget?.removeEventListener("cut", cutListener)
clipEventsTarget?.removeEventListener("copy", copyListener)
clipEventsTarget?.removeEventListener("paste", pasteListener)
clipEventsTarget?.removeEventListener("cut", cutListener)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,76 +17,24 @@
package androidx.compose.ui


import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
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
private val subscribeLock = Any()
private var currentSystemTheme = mutableStateOf(org.jetbrains.skiko.currentSystemTheme)

@OptIn(DelicateCoroutinesApi::class)
private fun onSubscriberAdded() {
synchronized(subscribeLock) {
if (subscriberCount == 0) {
pollingJob = GlobalScope.launch {
withContext(Dispatchers.IO) {
pollCurrentSystemTheme()
}
}
}
subscriberCount += 1
}
}

private fun onSubscriberRemoved() {
synchronized(subscribeLock) {
subscriberCount -= 1
if (subscriberCount == 0) {
pollingJob?.cancel()
pollingJob = null
}
}
}

private suspend fun pollCurrentSystemTheme() {
while (true) {
currentSystemTheme.value = org.jetbrains.skiko.currentSystemTheme
delay(1.seconds)
}
}
import androidx.compose.ui.platform.DesktopMediaEnvironment

@Composable
internal fun ProvideSystemTheme(content: @Composable () -> Unit) {
internal fun ProvideSystemTheme(mediaEnvironment: DesktopMediaEnvironment, content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalSystemTheme provides currentSystemTheme.value,
LocalSystemTheme provides mediaEnvironment.systemTheme ,
content = content
)

if (DesktopComposeUiFlags.pollSystemTheme) {
DisposableEffect(Unit) {
onSubscriberAdded()
mediaEnvironment.onSystemThemeSubscriberAdded()
onDispose {
onSubscriberRemoved()
mediaEnvironment.onSystemThemeSubscriberRemoved()
}
}
}
}

@VisibleForTesting
internal fun systemThemeSubscriberCount() = subscriberCount

@VisibleForTesting
internal fun systemThemePollingJob() = pollingJob
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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.annotation.VisibleForTesting
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.ExperimentalMediaQueryApi
import androidx.compose.ui.UiMediaScope
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.window.GlobalDensity
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.skiko.SystemTheme

internal class DesktopMediaEnvironment(val windowInfo: WindowInfo) : PlatformMediaEnvironment {



private var systemThemeSubscriberCount = 0
private var pollingSystemThemeJob: Job? = null
private val systemThemeSubscribeLock = Any()
private var currentSystemTheme = mutableStateOf(org.jetbrains.skiko.currentSystemTheme)

@OptIn(DelicateCoroutinesApi::class)
internal fun onSystemThemeSubscriberAdded() {
synchronized(systemThemeSubscribeLock) {
if (systemThemeSubscriberCount == 0) {
pollingSystemThemeJob = GlobalScope.launch {
withContext(Dispatchers.IO) {
pollCurrentSystemTheme()
}
}
}
systemThemeSubscriberCount += 1
}
}

internal fun onSystemThemeSubscriberRemoved() {
synchronized(systemThemeSubscribeLock) {
systemThemeSubscriberCount -= 1
if (systemThemeSubscriberCount == 0) {
pollingSystemThemeJob?.cancel()
pollingSystemThemeJob = null
}
}
}

private suspend fun pollCurrentSystemTheme() {
while (true) {
currentSystemTheme.value = org.jetbrains.skiko.currentSystemTheme
delay(1.seconds)
}
}

@VisibleForTesting
internal fun systemThemeSubscriberCount() = systemThemeSubscriberCount

@VisibleForTesting
internal fun systemThemePollingJob() = pollingSystemThemeJob

override val systemTheme: SystemTheme
get() = currentSystemTheme.value

override val systemDensity: Density
get() = GlobalDensity

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() {
pollingSystemThemeJob?.cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ 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
import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
import androidx.compose.ui.platform.PlatformComponent
import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.PlatformDragAndDropManager
import androidx.compose.ui.platform.PlatformMediaEnvironment
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
import androidx.compose.ui.platform.PlatformWindowContext
import androidx.compose.ui.platform.ViewConfiguration
Expand Down Expand Up @@ -164,6 +166,8 @@ internal class ComposeSceneMediator(
DesktopTextInputService2(platformComponent)
}

private val desktopMediaEnvironment = DesktopMediaEnvironment(windowInfo = windowContext.windowInfo)

// TODO: It must be shared between Compose instances.
// It's supposed to be stored in platform's root view or window.
val frameRecomposer = FrameRecomposer(coroutineContext, ::needRender)
Expand Down Expand Up @@ -628,6 +632,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 @@ -678,7 +684,7 @@ internal class ComposeSceneMediator(
runOnceComponentAttached {
catchExceptions {
scene.setContent {
ProvideSystemTheme {
ProvideSystemTheme(desktopMediaEnvironment) {
interopContainer {
content()
}
Expand Down Expand Up @@ -832,6 +838,7 @@ internal class ComposeSceneMediator(

private inner class DesktopPlatformContext : PlatformContext {
override val windowInfo: WindowInfo get() = windowContext.windowInfo
override val mediaEnvironment: PlatformMediaEnvironment 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 @@ -22,8 +22,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.ProvideSystemTheme
import androidx.compose.ui.pollSystemTheme
import androidx.compose.ui.systemThemePollingJob
import androidx.compose.ui.systemThemeSubscriberCount
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runComposeUiTest
import kotlin.test.Test
Expand All @@ -38,36 +36,37 @@ class SystemThemeTest {
val prevValue = ComposeUiFlags.pollSystemTheme
ComposeUiFlags.pollSystemTheme = true
try {
val desktopMediaEnvironment = DesktopMediaEnvironment(WindowInfoImpl())
runComposeUiTest {
var provideSystemTheme1 by mutableStateOf(false)
var provideSystemTheme2 by mutableStateOf(false)
setContent {
if (provideSystemTheme1) {
ProvideSystemTheme { }
ProvideSystemTheme(desktopMediaEnvironment) { }
}
if (provideSystemTheme2) {
ProvideSystemTheme { }
ProvideSystemTheme(desktopMediaEnvironment) { }
}
}

assertEquals(0, systemThemeSubscriberCount())
assertNull(systemThemePollingJob())
assertEquals(0, desktopMediaEnvironment.systemThemeSubscriberCount())
assertNull(desktopMediaEnvironment.systemThemePollingJob())

provideSystemTheme1 = true
waitForIdle()
assertEquals(1, systemThemeSubscriberCount())
assertNotNull(systemThemePollingJob())
assertEquals(1, desktopMediaEnvironment.systemThemeSubscriberCount())
assertNotNull(desktopMediaEnvironment.systemThemePollingJob())

provideSystemTheme2 = true
waitForIdle()
assertEquals(2, systemThemeSubscriberCount())
assertNotNull(systemThemePollingJob())
assertEquals(2, desktopMediaEnvironment.systemThemeSubscriberCount())
assertNotNull(desktopMediaEnvironment.systemThemePollingJob())

provideSystemTheme1 = false
provideSystemTheme2 = false
waitForIdle()
assertEquals(0, systemThemeSubscriberCount())
assertNull(systemThemePollingJob())
assertEquals(0, desktopMediaEnvironment.systemThemeSubscriberCount())
assertNull(desktopMediaEnvironment.systemThemePollingJob())
}
} finally {
ComposeUiFlags.pollSystemTheme = prevValue
Expand Down
Loading