diff --git a/compose/ui/ui/api/ui.klib.api b/compose/ui/ui/api/ui.klib.api index 6fead05e0645d..77b4e3629ea0c 100644 --- a/compose/ui/ui/api/ui.klib.api +++ b/compose/ui/ui/api/ui.klib.api @@ -991,12 +991,11 @@ abstract interface androidx.compose.ui.platform/AccessibilityManager { // androi abstract interface androidx.compose.ui.platform/Clipboard { // androidx.compose.ui.platform/Clipboard|null[0] open val nativeClipboard // androidx.compose.ui.platform/Clipboard.nativeClipboard|{}nativeClipboard[0] + open fun (): androidx.compose.ui.platform/W3CTemporaryClipboard // androidx.compose.ui.platform/Clipboard.nativeClipboard.|(){}[0] + // Targets: [ios] open fun (): platform.UIKit/UIPasteboard // androidx.compose.ui.platform/Clipboard.nativeClipboard.|(){}[0] - // Targets: [js, wasmJs] - open fun (): androidx.compose.ui.platform/W3CTemporaryClipboard // androidx.compose.ui.platform/Clipboard.nativeClipboard.|(){}[0] - // Targets: [macosArm64] open fun (): platform.AppKit/NSPasteboard // androidx.compose.ui.platform/Clipboard.nativeClipboard.|(){}[0] @@ -1006,12 +1005,11 @@ abstract interface androidx.compose.ui.platform/Clipboard { // androidx.compose. abstract interface androidx.compose.ui.platform/ClipboardManager { // androidx.compose.ui.platform/ClipboardManager|null[0] open val nativeClipboard // androidx.compose.ui.platform/ClipboardManager.nativeClipboard|{}nativeClipboard[0] + open fun (): androidx.compose.ui.platform/W3CTemporaryClipboard // androidx.compose.ui.platform/ClipboardManager.nativeClipboard.|(){}[0] + // Targets: [ios] open fun (): platform.UIKit/UIPasteboard // androidx.compose.ui.platform/ClipboardManager.nativeClipboard.|(){}[0] - // Targets: [js, wasmJs] - open fun (): androidx.compose.ui.platform/W3CTemporaryClipboard // androidx.compose.ui.platform/ClipboardManager.nativeClipboard.|(){}[0] - // Targets: [macosArm64] open fun (): platform.AppKit/NSPasteboard // androidx.compose.ui.platform/ClipboardManager.nativeClipboard.|(){}[0] @@ -2172,13 +2170,12 @@ final class androidx.compose.ui.platform/ClipEntry { // androidx.compose.ui.plat final val clipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata|{}clipMetadata[0] final fun (): androidx.compose.ui.platform/ClipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata.|(){}[0] - // Targets: [apple] - final object Companion // androidx.compose.ui.platform/ClipEntry.Companion|null[0] - - // Targets: [js, wasmJs] final object Companion { // androidx.compose.ui.platform/ClipEntry.Companion|null[0] final fun withPlainText(kotlin/String): androidx.compose.ui.platform/ClipEntry // androidx.compose.ui.platform/ClipEntry.Companion.withPlainText|withPlainText(kotlin.String){}[0] } + + // Targets: [apple] + final object Companion // androidx.compose.ui.platform/ClipEntry.Companion|null[0] } final class androidx.compose.ui.platform/ClipMetadata // androidx.compose.ui.platform/ClipMetadata|null[0] @@ -3674,6 +3671,8 @@ sealed class androidx.compose.ui.layout/Ruler // androidx.compose.ui.layout/Rule sealed class androidx.compose.ui.modifier/ModifierLocalMap // androidx.compose.ui.modifier/ModifierLocalMap|null[0] +final object androidx.compose.ui.input.pointer/DummyPointerIcon : androidx.compose.ui.input.pointer/PointerIcon // androidx.compose.ui.input.pointer/DummyPointerIcon|null[0] + final object androidx.compose.ui.semantics/SemanticsActions { // androidx.compose.ui.semantics/SemanticsActions|null[0] final val ClearTextSubstitution // androidx.compose.ui.semantics/SemanticsActions.ClearTextSubstitution|{}ClearTextSubstitution[0] final fun (): androidx.compose.ui.semantics/SemanticsPropertyKey>> // androidx.compose.ui.semantics/SemanticsActions.ClearTextSubstitution.|(){}[0] @@ -3885,6 +3884,10 @@ final const val androidx.compose.ui.graphics/DefaultCameraDistance // androidx.c final val androidx.compose.ui.autofill/androidx_compose_ui_autofill_AutofillManager$stableprop // androidx.compose.ui.autofill/androidx_compose_ui_autofill_AutofillManager$stableprop|#static{}androidx_compose_ui_autofill_AutofillManager$stableprop[0] final val androidx.compose.ui.autofill/androidx_compose_ui_autofill_AutofillNode$stableprop // androidx.compose.ui.autofill/androidx_compose_ui_autofill_AutofillNode$stableprop|#static{}androidx_compose_ui_autofill_AutofillNode$stableprop[0] final val androidx.compose.ui.autofill/androidx_compose_ui_autofill_AutofillTree$stableprop // androidx.compose.ui.autofill/androidx_compose_ui_autofill_AutofillTree$stableprop|#static{}androidx_compose_ui_autofill_AutofillTree$stableprop[0] +final val androidx.compose.ui.dom/domEventOrNull // androidx.compose.ui.dom/domEventOrNull|@androidx.compose.ui.input.key.KeyEvent{}domEventOrNull[0] + final fun (androidx.compose.ui.input.key/KeyEvent).(): org.w3c.dom.events/KeyboardEvent? // androidx.compose.ui.dom/domEventOrNull.|@androidx.compose.ui.input.key.KeyEvent(){}[0] +final val androidx.compose.ui.dom/domEventOrNull // androidx.compose.ui.dom/domEventOrNull|@androidx.compose.ui.input.pointer.PointerEvent{}domEventOrNull[0] + final fun (androidx.compose.ui.input.pointer/PointerEvent).(): org.w3c.dom.events/Event? // androidx.compose.ui.dom/domEventOrNull.|@androidx.compose.ui.input.pointer.PointerEvent(){}[0] final val androidx.compose.ui.draganddrop/androidx_compose_ui_draganddrop_DragAndDropEvent$stableprop // androidx.compose.ui.draganddrop/androidx_compose_ui_draganddrop_DragAndDropEvent$stableprop|#static{}androidx_compose_ui_draganddrop_DragAndDropEvent$stableprop[0] final val androidx.compose.ui.draganddrop/androidx_compose_ui_draganddrop_DragAndDropTransferData$stableprop // androidx.compose.ui.draganddrop/androidx_compose_ui_draganddrop_DragAndDropTransferData$stableprop|#static{}androidx_compose_ui_draganddrop_DragAndDropTransferData$stableprop[0] final val androidx.compose.ui.draw/androidx_compose_ui_draw_CacheDrawScope$stableprop // androidx.compose.ui.draw/androidx_compose_ui_draw_CacheDrawScope$stableprop|#static{}androidx_compose_ui_draw_CacheDrawScope$stableprop[0] @@ -3951,6 +3954,7 @@ final val androidx.compose.ui.input.nestedscroll/androidx_compose_ui_input_neste final val androidx.compose.ui.input.pointer.util/androidx_compose_ui_input_pointer_util_VelocityTracker$stableprop // androidx.compose.ui.input.pointer.util/androidx_compose_ui_input_pointer_util_VelocityTracker$stableprop|#static{}androidx_compose_ui_input_pointer_util_VelocityTracker$stableprop[0] final val androidx.compose.ui.input.pointer.util/androidx_compose_ui_input_pointer_util_VelocityTracker1D$stableprop // androidx.compose.ui.input.pointer.util/androidx_compose_ui_input_pointer_util_VelocityTracker1D$stableprop|#static{}androidx_compose_ui_input_pointer_util_VelocityTracker1D$stableprop[0] final val androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_ConsumedData$stableprop // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_ConsumedData$stableprop|#static{}androidx_compose_ui_input_pointer_ConsumedData$stableprop[0] +final val androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop|#static{}androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop[0] final val androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_HistoricalChange$stableprop // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_HistoricalChange$stableprop|#static{}androidx_compose_ui_input_pointer_HistoricalChange$stableprop[0] final val androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_PointerEvent$stableprop // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_PointerEvent$stableprop|#static{}androidx_compose_ui_input_pointer_PointerEvent$stableprop[0] final val androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_PointerEventTimeoutCancellationException$stableprop // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_PointerEventTimeoutCancellationException$stableprop|#static{}androidx_compose_ui_input_pointer_PointerEventTimeoutCancellationException$stableprop[0] @@ -4107,6 +4111,9 @@ final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformCont final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop|#static{}androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop[0] final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop|#static{}androidx_compose_ui_platform_ValueElement$stableprop[0] final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop|#static{}androidx_compose_ui_platform_ValueElementSequence$stableprop[0] +final val androidx.compose.ui.platform/androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop|#static{}androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop[0] +final val androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar$stableprop|#static{}androidx_compose_ui_platform_WebTextToolbar$stableprop[0] +final val androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop|#static{}androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop[0] final val androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop|#static{}androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop[0] final val androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneDragAndDropNode$stableprop // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneDragAndDropNode$stableprop|#static{}androidx_compose_ui_scene_ComposeSceneDragAndDropNode$stableprop[0] final val androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneFocusManager$stableprop // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneFocusManager$stableprop|#static{}androidx_compose_ui_scene_ComposeSceneFocusManager$stableprop[0] @@ -4126,7 +4133,11 @@ final val androidx.compose.ui.semantics/androidx_compose_ui_semantics_SemanticsO final val androidx.compose.ui.semantics/androidx_compose_ui_semantics_SemanticsProperties$stableprop // androidx.compose.ui.semantics/androidx_compose_ui_semantics_SemanticsProperties$stableprop|#static{}androidx_compose_ui_semantics_SemanticsProperties$stableprop[0] final val androidx.compose.ui.semantics/androidx_compose_ui_semantics_SemanticsPropertyKey$stableprop // androidx.compose.ui.semantics/androidx_compose_ui_semantics_SemanticsPropertyKey$stableprop|#static{}androidx_compose_ui_semantics_SemanticsPropertyKey$stableprop[0] final val androidx.compose.ui.spatial/androidx_compose_ui_spatial_RelativeLayoutBounds$stableprop // androidx.compose.ui.spatial/androidx_compose_ui_spatial_RelativeLayoutBounds$stableprop|#static{}androidx_compose_ui_spatial_RelativeLayoutBounds$stableprop[0] +final val androidx.compose.ui.window/androidx_compose_ui_window_ComposeViewportConfiguration$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_ComposeViewportConfiguration$stableprop|#static{}androidx_compose_ui_window_ComposeViewportConfiguration$stableprop[0] final val androidx.compose.ui.window/androidx_compose_ui_window_DialogProperties$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_DialogProperties$stableprop|#static{}androidx_compose_ui_window_DialogProperties$stableprop[0] +final val androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop|#static{}androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop[0] +final val androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop|#static{}androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop[0] +final val androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop|#static{}androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop[0] final val androidx.compose.ui.window/androidx_compose_ui_window_PopupProperties$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_PopupProperties$stableprop|#static{}androidx_compose_ui_window_PopupProperties$stableprop[0] final val androidx.compose.ui/LocalUiMediaScope // androidx.compose.ui/LocalUiMediaScope|{}LocalUiMediaScope[0] final fun (): androidx.compose.runtime/ProvidableCompositionLocal // androidx.compose.ui/LocalUiMediaScope.|(){}[0] @@ -4520,6 +4531,7 @@ final fun androidx.compose.ui.input.pointer/PointerKeyboardModifiers(kotlin/Bool final fun androidx.compose.ui.input.pointer/SuspendingPointerInputModifierNode(androidx.compose.ui.input.pointer/PointerInputEventHandler): androidx.compose.ui.input.pointer/SuspendingPointerInputModifierNode // androidx.compose.ui.input.pointer/SuspendingPointerInputModifierNode|SuspendingPointerInputModifierNode(androidx.compose.ui.input.pointer.PointerInputEventHandler){}[0] final fun androidx.compose.ui.input.pointer/SuspendingPointerInputModifierNode(kotlin.coroutines/SuspendFunction1): androidx.compose.ui.input.pointer/SuspendingPointerInputModifierNode // androidx.compose.ui.input.pointer/SuspendingPointerInputModifierNode|SuspendingPointerInputModifierNode(kotlin.coroutines.SuspendFunction1){}[0] final fun androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_ConsumedData$stableprop_getter(): kotlin/Int // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_ConsumedData$stableprop_getter|androidx_compose_ui_input_pointer_ConsumedData$stableprop_getter(){}[0] +final fun androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop_getter(): kotlin/Int // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop_getter|androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop_getter(){}[0] final fun androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_HistoricalChange$stableprop_getter(): kotlin/Int // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_HistoricalChange$stableprop_getter|androidx_compose_ui_input_pointer_HistoricalChange$stableprop_getter(){}[0] final fun androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_PointerEvent$stableprop_getter(): kotlin/Int // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_PointerEvent$stableprop_getter|androidx_compose_ui_input_pointer_PointerEvent$stableprop_getter(){}[0] final fun androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_PointerEventTimeoutCancellationException$stableprop_getter(): kotlin/Int // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_PointerEventTimeoutCancellationException$stableprop_getter|androidx_compose_ui_input_pointer_PointerEventTimeoutCancellationException$stableprop_getter(){}[0] @@ -4583,6 +4595,9 @@ final fun androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformCont final fun androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop_getter|androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop_getter(){}[0] final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop_getter|androidx_compose_ui_platform_ValueElement$stableprop_getter(){}[0] final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter|androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(){}[0] +final fun androidx.compose.ui.platform/androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop_getter|androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop_getter(){}[0] +final fun androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar$stableprop_getter|androidx_compose_ui_platform_WebTextToolbar$stableprop_getter(){}[0] +final fun androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop_getter|androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop_getter(){}[0] final fun androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop_getter(): kotlin/Int // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop_getter|androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop_getter(){}[0] final fun androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneDragAndDropNode$stableprop_getter(): kotlin/Int // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneDragAndDropNode$stableprop_getter|androidx_compose_ui_scene_ComposeSceneDragAndDropNode$stableprop_getter(){}[0] final fun androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneFocusManager$stableprop_getter(): kotlin/Int // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneFocusManager$stableprop_getter|androidx_compose_ui_scene_ComposeSceneFocusManager$stableprop_getter(){}[0] @@ -4611,7 +4626,11 @@ final fun androidx.compose.ui.window/Popup(androidx.compose.ui.window/PopupPosit final fun androidx.compose.ui.window/Popup(androidx.compose.ui/Alignment?, androidx.compose.ui.unit/IntOffset, kotlin/Boolean, kotlin/Function0?, kotlin/Function1?, kotlin/Function1?, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.ui.window/Popup|Popup(androidx.compose.ui.Alignment?;androidx.compose.ui.unit.IntOffset;kotlin.Boolean;kotlin.Function0?;kotlin.Function1?;kotlin.Function1?;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.ui.window/Popup(androidx.compose.ui/Alignment?, androidx.compose.ui.unit/IntOffset, kotlin/Function0?, androidx.compose.ui.window/PopupProperties?, kotlin/Function1?, kotlin/Function1?, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.ui.window/Popup|Popup(androidx.compose.ui.Alignment?;androidx.compose.ui.unit.IntOffset;kotlin.Function0?;androidx.compose.ui.window.PopupProperties?;kotlin.Function1?;kotlin.Function1?;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.ui.window/Popup(androidx.compose.ui/Alignment?, androidx.compose.ui.unit/IntOffset, kotlin/Function0?, androidx.compose.ui.window/PopupProperties?, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.ui.window/Popup|Popup(androidx.compose.ui.Alignment?;androidx.compose.ui.unit.IntOffset;kotlin.Function0?;androidx.compose.ui.window.PopupProperties?;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun androidx.compose.ui.window/androidx_compose_ui_window_ComposeViewportConfiguration$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_ComposeViewportConfiguration$stableprop_getter|androidx_compose_ui_window_ComposeViewportConfiguration$stableprop_getter(){}[0] final fun androidx.compose.ui.window/androidx_compose_ui_window_DialogProperties$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_DialogProperties$stableprop_getter|androidx_compose_ui_window_DialogProperties$stableprop_getter(){}[0] +final fun androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop_getter|androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop_getter(){}[0] +final fun androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop_getter|androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop_getter(){}[0] +final fun androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop_getter|androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop_getter(){}[0] final fun androidx.compose.ui.window/androidx_compose_ui_window_PopupProperties$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_PopupProperties$stableprop_getter|androidx_compose_ui_window_PopupProperties$stableprop_getter(){}[0] final fun androidx.compose.ui/androidx_compose_ui_AbsoluteAlignment$stableprop_getter(): kotlin/Int // androidx.compose.ui/androidx_compose_ui_AbsoluteAlignment$stableprop_getter|androidx_compose_ui_AbsoluteAlignment$stableprop_getter(){}[0] final fun androidx.compose.ui/androidx_compose_ui_BiasAbsoluteAlignment$stableprop_getter(): kotlin/Int // androidx.compose.ui/androidx_compose_ui_BiasAbsoluteAlignment$stableprop_getter|androidx_compose_ui_BiasAbsoluteAlignment$stableprop_getter(){}[0] @@ -4908,77 +4927,6 @@ final fun androidx.compose.ui.window/ComposeUIViewController(kotlin/Function1): platform.UIKit/UIViewController // androidx.compose.ui.window/ComposeUIViewController|ComposeUIViewController(kotlin.Function2){}[0] -// Targets: [js, wasmJs] -final object androidx.compose.ui.input.pointer/DummyPointerIcon : androidx.compose.ui.input.pointer/PointerIcon // androidx.compose.ui.input.pointer/DummyPointerIcon|null[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.dom/domEventOrNull // androidx.compose.ui.dom/domEventOrNull|@androidx.compose.ui.input.key.KeyEvent{}domEventOrNull[0] - final fun (androidx.compose.ui.input.key/KeyEvent).(): org.w3c.dom.events/KeyboardEvent? // androidx.compose.ui.dom/domEventOrNull.|@androidx.compose.ui.input.key.KeyEvent(){}[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.dom/domEventOrNull // androidx.compose.ui.dom/domEventOrNull|@androidx.compose.ui.input.pointer.PointerEvent{}domEventOrNull[0] - final fun (androidx.compose.ui.input.pointer/PointerEvent).(): org.w3c.dom.events/Event? // androidx.compose.ui.dom/domEventOrNull.|@androidx.compose.ui.input.pointer.PointerEvent(){}[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop|#static{}androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.platform/androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop|#static{}androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar$stableprop|#static{}androidx_compose_ui_platform_WebTextToolbar$stableprop[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop|#static{}androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.window/androidx_compose_ui_window_ComposeViewportConfiguration$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_ComposeViewportConfiguration$stableprop|#static{}androidx_compose_ui_window_ComposeViewportConfiguration$stableprop[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.window/androidx_compose_ui_window_KeyboardModeState_Hardware$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_KeyboardModeState_Hardware$stableprop|#static{}androidx_compose_ui_window_KeyboardModeState_Hardware$stableprop[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.window/androidx_compose_ui_window_KeyboardModeState_Virtual$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_KeyboardModeState_Virtual$stableprop|#static{}androidx_compose_ui_window_KeyboardModeState_Virtual$stableprop[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop|#static{}androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop|#static{}androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop[0] - -// Targets: [js, wasmJs] -final val androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop|#static{}androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop_getter(): kotlin/Int // androidx.compose.ui.input.pointer/androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop_getter|androidx_compose_ui_input_pointer_DummyPointerIcon$stableprop_getter(){}[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.platform/androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop_getter|androidx_compose_ui_platform_W3CTemporaryClipboard$stableprop_getter(){}[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar$stableprop_getter|androidx_compose_ui_platform_WebTextToolbar$stableprop_getter(){}[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop_getter|androidx_compose_ui_platform_WebTextToolbar_TextToolbarMenu$stableprop_getter(){}[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.window/androidx_compose_ui_window_ComposeViewportConfiguration$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_ComposeViewportConfiguration$stableprop_getter|androidx_compose_ui_window_ComposeViewportConfiguration$stableprop_getter(){}[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.window/androidx_compose_ui_window_KeyboardModeState_Hardware$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_KeyboardModeState_Hardware$stableprop_getter|androidx_compose_ui_window_KeyboardModeState_Hardware$stableprop_getter(){}[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.window/androidx_compose_ui_window_KeyboardModeState_Virtual$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_KeyboardModeState_Virtual$stableprop_getter|androidx_compose_ui_window_KeyboardModeState_Virtual$stableprop_getter(){}[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop_getter|androidx_compose_ui_window_MediaQueryStatus_MATCH$stableprop_getter(){}[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop_getter|androidx_compose_ui_window_MediaQueryStatus_NO_MATCH$stableprop_getter(){}[0] - -// Targets: [js, wasmJs] -final fun androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop_getter(): kotlin/Int // androidx.compose.ui.window/androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop_getter|androidx_compose_ui_window_MediaQueryStatus_UNSUPPORTED$stableprop_getter(){}[0] - // Targets: [macosArm64] abstract interface androidx.compose.ui.window/WindowScope { // androidx.compose.ui.window/WindowScope|null[0] abstract val window // androidx.compose.ui.window/WindowScope.window|{}window[0] 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 344bb976970cf..c2e7ba1c9aa50 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 @@ -164,6 +167,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) @@ -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 } @@ -832,6 +839,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 diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/platform/MediaEnvironment.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/platform/MediaEnvironment.ios.kt new file mode 100644 index 0000000000000..3079ea28cc16f --- /dev/null +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/platform/MediaEnvironment.ios.kt @@ -0,0 +1,285 @@ +/* + * 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.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.ExperimentalMediaQueryApi +import androidx.compose.ui.UiMediaScope +import androidx.compose.ui.uikit.InterfaceOrientation +import androidx.compose.ui.uikit.density +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import org.jetbrains.skiko.SystemTheme +import androidx.compose.ui.uikit.utils.CMPKeyValueObserver +import androidx.compose.ui.uikit.utils.CMPUIWindowSceneUtils +import androidx.compose.ui.window.KeyboardVisibilityListener +import androidx.compose.ui.window.KeyboardVisibilityObserver +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.CPointed +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ObjCAction +import kotlinx.cinterop.useContents +import platform.AVFoundation.AVCaptureDevice +import platform.AVFoundation.AVCaptureDeviceWasConnectedNotification +import platform.AVFoundation.AVCaptureDeviceWasDisconnectedNotification +import platform.AVFoundation.AVMediaTypeAudio +import platform.AVFoundation.AVMediaTypeVideo +import platform.CoreGraphics.CGRect +import platform.Foundation.NSKeyValueObservingOptionNew +import platform.Foundation.NSNotification +import platform.Foundation.NSNotificationCenter +import platform.Foundation.NSSelectorFromString +import platform.Foundation.addObserver +import platform.Foundation.removeObserver +import platform.darwin.NSObject +import platform.UIKit.UIUserInterfaceStyle +import platform.UIKit.UIViewAnimationOptions +import platform.UIKit.UIWindow +import platform.UIKit.UIWindowScene + +internal class MediaEnvironment(val windowInfo: WindowInfo) : UiMediaScope, KeyboardVisibilityObserver { + + private var window: UIWindow? = null + /* + * Initial value is arbitrarily chosen to avoid propagating invalid value logic + * It's never the case in the real usage scenario to reflect that in type system + */ + internal val interfaceOrientationState: MutableState = mutableStateOf( + InterfaceOrientation.Portrait + ) + private val systemThemeState: MutableState = mutableStateOf(SystemTheme.UNKNOWN) + + private val systemDensityState: MutableState = mutableStateOf(window?.density ?: Density(1f)) + + private val isImeShowing = mutableStateOf(KeyboardVisibilityListener.keyboardFrame.useContents { size.height > 0 }) + private val pointerPrecisionState: MutableState = mutableStateOf( + UiMediaScope.PointerPrecision.Coarse + ) + + private val hasMicrophoneState: MutableState = mutableStateOf(detectHasMicrophone()) + private val hasCameraState: MutableState = mutableStateOf(detectHasCamera()) + + private fun detectHasMicrophone(): Boolean = + AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeAudio) != null + + private fun detectHasCamera(): Boolean = + AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) != null + + + private fun updateCaptureDeviceAvailabilityState() { + hasMicrophoneState.value = detectHasMicrophone() + hasCameraState.value = detectHasCamera() + } + + private val captureDeviceAvailabilityObserver = CaptureDeviceAvailabilityObserver(::updateCaptureDeviceAvailabilityState) + + fun updateInterfaceOrientationState() { + currentInterfaceOrientation?.let { + interfaceOrientationState.value = it + } + } + fun updateUserInterfaceStyle(style: UIUserInterfaceStyle) { + systemThemeState.value = style.asComposeSystemTheme() + } + fun updatePointerPrecision(precision: UiMediaScope.PointerPrecision) { + pointerPrecisionState.value = precision + } + + private val currentInterfaceOrientation: InterfaceOrientation? + get() { + return InterfaceOrientation.getByRawValue( + CMPUIWindowSceneUtils.interfaceOrientationForWindowScene(window?.windowScene) + ) + } + + private val interfaceOrientationObserver = SceneGeometryObserver { + updateInterfaceOrientationState() + } + fun onDidMoveToWindow(window: UIWindow?) { + this.window = window + interfaceOrientationObserver.windowScene = window?.windowScene + window ?: return + + systemDensityState.value = window.density + updateInterfaceOrientationState() + + } + + val systemTheme: SystemTheme + get() = systemThemeState.value + val systemDensity: Density + get() = systemDensityState.value + + fun startObserving() { + interfaceOrientationObserver.isObservingEnabled = true + captureDeviceAvailabilityObserver.isObservingEnabled = true + KeyboardVisibilityListener.addObserver(this) + } + + fun stopObserving() { + interfaceOrientationObserver.isObservingEnabled = false + captureDeviceAvailabilityObserver.isObservingEnabled = false + KeyboardVisibilityListener.removeObserver(this) + } + + override val windowPosture: UiMediaScope.Posture + get() = UiMediaScope.Posture.Flat //iOS doesn't have foldables yet! + override val windowWidth: Dp + get() = windowInfo.containerDpSize.width + override val windowHeight: Dp + get() = windowInfo.containerDpSize.height + override val pointerPrecision: UiMediaScope.PointerPrecision + get() = pointerPrecisionState.value + override val keyboardKind: UiMediaScope.KeyboardKind + get() = when { + isImeShowing.value -> UiMediaScope.KeyboardKind.Virtual + else -> UiMediaScope.KeyboardKind.None + } + override val hasMicrophone: Boolean + get() = hasMicrophoneState.value + override val hasCamera: Boolean + get() = hasCameraState.value + override val viewingDistance: UiMediaScope.ViewingDistance + get() = UiMediaScope.ViewingDistance.Near + + override fun keyboardWillShow( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + isImeShowing.value = targetFrame.useContents { size.height > 0 } + } + + override fun keyboardWillHide( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + isImeShowing.value = false //targetFrame is CGRectZero.readValue() + } + + override fun keyboardWillChangeFrame( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + isImeShowing.value = targetFrame.useContents { size.height > 0 } + } +} + + +private class SceneGeometryObserver( + val onGeometryChanged: () -> Unit +) : CMPKeyValueObserver() { + private val observingKey = "effectiveGeometry" + + var windowScene: UIWindowScene? = null + set(value) { + if (field == value) return + removeObserverIfNeeded() + field = value + addObserverIfNeeded() + } + + var isObservingEnabled = false + set(value) { + if (field == value) return + field = value + if (value) { + addObserverIfNeeded() + } else { + removeObserverIfNeeded() + } + } + + private var isObservingAdded = false + + private fun addObserverIfNeeded() { + if (isObservingEnabled && !isObservingAdded) { + isObservingAdded = true + windowScene?.addObserver(this, observingKey, NSKeyValueObservingOptionNew, null) + } + } + + private fun removeObserverIfNeeded() { + windowScene?.removeObserver(this, observingKey) + isObservingAdded = false + } + + override fun observeValueForKeyPath( + keyPath: String?, + ofObject: Any?, + change: Map?, + context: CPointer? + ) { + onGeometryChanged() + } +} + +private class CaptureDeviceAvailabilityObserver( + val onDeviceAvailabilityChanged: () -> Unit, + private val notificationCenter: NSNotificationCenter = NSNotificationCenter.defaultCenter +) : NSObject() { + + var isObservingEnabled = false + set(value) { + if (field == value) return + field = value + if (value) { + addObservers() + } else { + removeObservers() + } + } + + private fun addObservers() { + notificationCenter.addObserver( + observer = this, + selector = NSSelectorFromString(::deviceAvailabilityDidChange.name + ":"), + name = AVCaptureDeviceWasConnectedNotification, + `object` = null + ) + notificationCenter.addObserver( + observer = this, + selector = NSSelectorFromString(::deviceAvailabilityDidChange.name + ":"), + name = AVCaptureDeviceWasDisconnectedNotification, + `object` = null + ) + } + + private fun removeObservers() { + notificationCenter.removeObserver(this) + } + + @OptIn(BetaInteropApi::class) + @ObjCAction + fun deviceAvailabilityDidChange(arg: NSNotification) { + onDeviceAvailabilityChanged() + } +} + +private fun UIUserInterfaceStyle.asComposeSystemTheme(): SystemTheme { + return when (this) { + UIUserInterfaceStyle.UIUserInterfaceStyleLight -> SystemTheme.LIGHT + UIUserInterfaceStyle.UIUserInterfaceStyleDark -> SystemTheme.DARK + else -> SystemTheme.UNKNOWN + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt index 752e552b576fd..2167eb6fa0690 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt @@ -19,25 +19,22 @@ package androidx.compose.ui.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.LocalSystemTheme import androidx.compose.ui.graphics.asComposeCanvas import androidx.compose.ui.navigationevent.UIKitNavigationEventInput import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner +import androidx.compose.ui.platform.FrameRecomposer +import androidx.compose.ui.platform.MediaEnvironment import androidx.compose.ui.platform.FrameChoreographer import androidx.compose.ui.platform.MotionDurationScaleImpl import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformWindowContext import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.uikit.ComposeContainerConfiguration -import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalUIViewController import androidx.compose.ui.uikit.PlistSanityCheck import androidx.compose.ui.uikit.density import androidx.compose.ui.uikit.embedSubview -import androidx.compose.ui.uikit.utils.CMPKeyValueObserver -import androidx.compose.ui.uikit.utils.CMPUIWindowSceneUtils import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection @@ -131,9 +128,8 @@ internal class ComposeContainer( get() = mediatorComponentsOwner ?: error("ArchitectureComponentsOwner is not initialized yet.") - private val interfaceOrientationObserver = SceneGeometryObserver { - updateInterfaceOrientationState() - } + private val mediaEnvironment = MediaEnvironment(windowContext.windowInfo) + private val navigationEventInput = UIKitNavigationEventInput( density = view.density, initialLayoutDirection = layoutDirection, @@ -143,14 +139,6 @@ internal class ComposeContainer( private var layoutInvalidationHandler: LayoutInvalidationHandler? = null val hasInteropViews: Boolean get() = mediator?.hasInteropViews ?: false - /* - * Initial value is arbitrarily chosen to avoid propagating invalid value logic - * It's never the case in the real usage scenario to reflect that in type system - */ - private val interfaceOrientationState: MutableState = mutableStateOf( - InterfaceOrientation.Portrait - ) - private val systemThemeState: MutableState = mutableStateOf(SystemTheme.UNKNOWN) private val focusedViewsList = FocusedViewsList() @@ -181,12 +169,6 @@ internal class ComposeContainer( return mediator?.hasInvalidations == true || layersHolder?.layersViewController?.hasInvalidations == true } - private val currentInterfaceOrientation: InterfaceOrientation? - get() { - return InterfaceOrientation.getByRawValue( - CMPUIWindowSceneUtils.interfaceOrientationForWindowScene(windowScene) - ) - } private fun onLayoutSubviews() { windowContext.updateWindowContainerSize() @@ -200,23 +182,16 @@ internal class ComposeContainer( private fun onDidMoveToWindow(window: UIWindow?) { navigationEventInput.onDidMoveToWindow(window, view) - interfaceOrientationObserver.windowScene = window?.windowScene - + mediaEnvironment.onDidMoveToWindow(window) window ?: return - updateInterfaceOrientationState() - layersHolder?.layersViewController?.containerWindow = view.window windowContext.window = window updateMotionSpeed() lifecycleDelegate.windowScene = window.windowScene } - fun updateInterfaceOrientationState() { - currentInterfaceOrientation?.let { - interfaceOrientationState.value = it - } - } + fun updateInterfaceOrientationState() = mediaEnvironment.updateInterfaceOrientationState() fun sceneDidAppear() { mediator?.sceneDidAppear() @@ -235,9 +210,7 @@ internal class ComposeContainer( navigationEventInput.onDidMoveToWindow(null, view) } - fun updateUserInterfaceStyle(style: UIUserInterfaceStyle) { - systemThemeState.value = style.asComposeSystemTheme() - } + fun updateUserInterfaceStyle(style: UIUserInterfaceStyle) = mediaEnvironment.updateUserInterfaceStyle(style) fun initializeComposeScene() { sceneJob = Job() @@ -308,7 +281,7 @@ internal class ComposeContainer( ) }, navigationEventInput = navigationEventInput, - interfaceOrientationState = interfaceOrientationState, + mediaEnvironment = mediaEnvironment, ).also { mediator -> view.embedSubview(mediator.backgroundView) view.updateMetalView( @@ -332,7 +305,7 @@ internal class ComposeContainer( } } - interfaceOrientationObserver.isObservingEnabled = true + mediaEnvironment.startObserving() architectureComponentsOwner.navigationEventDispatcher.addInput(navigationEventInput) lifecycleDelegate.windowScene = windowScene @@ -359,7 +332,7 @@ internal class ComposeContainer( layersHolder = null - interfaceOrientationObserver.isObservingEnabled = false + mediaEnvironment.stopObserving() } private fun createComposeSceneContext( @@ -399,7 +372,7 @@ internal class ComposeContainer( consumePointerInputOutside = consumePointerInputOutside, parentCoroutineContext = containerCoroutineContext, ownerProvider = architectureComponentsOwner, - interfaceOrientationState = interfaceOrientationState, + mediaEnvironment = mediaEnvironment, invalidateLayout = { layersHolder.getLayersViewController().invalidateLayout() }, invalidateDraw = { layersHolder.getLayersViewController().invalidateDraw() }, ) @@ -442,7 +415,7 @@ internal class ComposeContainer( private fun ProvideContainerCompositionLocals(content: @Composable () -> Unit) = CompositionLocalProvider( LocalUIViewController provides containingViewController, - LocalSystemTheme provides systemThemeState.value, + LocalSystemTheme provides mediaEnvironment.systemTheme, content = content ) @@ -460,14 +433,6 @@ internal class ComposeContainer( get() = view.window?.windowScene } -private fun UIUserInterfaceStyle.asComposeSystemTheme(): SystemTheme { - return when (this) { - UIUserInterfaceStyle.UIUserInterfaceStyleLight -> SystemTheme.LIGHT - UIUserInterfaceStyle.UIUserInterfaceStyleDark -> SystemTheme.DARK - else -> SystemTheme.UNKNOWN - } -} - private fun getApplicationLayoutDirection() = when (UIApplication.sharedApplication().userInterfaceLayoutDirection) { UIUserInterfaceLayoutDirectionRightToLeft -> LayoutDirection.Rtl @@ -496,53 +461,6 @@ private class ComposeLayersHolder( } } -private class SceneGeometryObserver( - val onGeometryChanged: () -> Unit -) : CMPKeyValueObserver() { - private val observingKey = "effectiveGeometry" - - var windowScene: UIWindowScene? = null - set(value) { - if (field == value) return - removeObserverIfNeeded() - field = value - addObserverIfNeeded() - } - - var isObservingEnabled = false - set(value) { - if (field == value) return - field = value - if (value) { - addObserverIfNeeded() - } else { - removeObserverIfNeeded() - } - } - - private var isObservingAdded = false - - private fun addObserverIfNeeded() { - if (isObservingEnabled && !isObservingAdded) { - isObservingAdded = true - windowScene?.addObserver(this, observingKey, NSKeyValueObservingOptionNew, null) - } - } - - private fun removeObserverIfNeeded() { - windowScene?.removeObserver(this, observingKey) - isObservingAdded = false - } - - override fun observeValueForKeyPath( - keyPath: String?, - ofObject: Any?, - change: Map?, - context: CPointer? - ) { - onGeometryChanged() - } -} private fun UIUserInterfaceLayoutDirection.asLayoutDirection(): LayoutDirection = when (this) { UIUserInterfaceLayoutDirectionLeftToRight -> LayoutDirection.Ltr diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.ios.kt index ee89f7c651c52..c69d4c85c44c5 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.ios.kt @@ -20,11 +20,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalContext import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.UiMediaScope import androidx.compose.ui.draganddrop.UIKitDragAndDropManager import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect @@ -57,6 +58,8 @@ import androidx.compose.ui.navigationevent.UIKitNavigationEventInput import androidx.compose.ui.platform.AccessibilityMediator import androidx.compose.ui.platform.CUPERTINO_TOUCH_SLOP import androidx.compose.ui.platform.DefaultInputModeManager +import androidx.compose.ui.platform.FrameRecomposer +import androidx.compose.ui.platform.MediaEnvironment import androidx.compose.ui.platform.FrameChoreographer import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner import androidx.compose.ui.platform.PlatformContext @@ -69,7 +72,6 @@ import androidx.compose.ui.platform.UIKitWindowInsetsManager import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalNativeTextInputContext import androidx.compose.ui.uikit.LocalUIView import androidx.compose.ui.uikit.OnFocusBehavior @@ -195,7 +197,7 @@ internal class ComposeSceneMediator( private val architectureComponentsOwner: PlatformArchitectureComponentsOwner, val coroutineContext: CoroutineContext, private val navigationEventInput: UIKitNavigationEventInput, - interfaceOrientationState: State, + private val mediaEnvironment: MediaEnvironment, composeSceneFactory: (platformContext: PlatformContext) -> ComposeScene, ) { private var onPreviewKeyEvent: (KeyEvent) -> Boolean = { false } @@ -378,7 +380,7 @@ internal class ComposeSceneMediator( { _overlayView }, { windowContext.window?.rootViewController?.view }, ), - interfaceOrientation = interfaceOrientationState + interfaceOrientation = mediaEnvironment.interfaceOrientationState ) /** @@ -486,12 +488,14 @@ internal class ComposeSceneMediator( } } + @OptIn(ExperimentalMediaQueryApi::class) private fun onScrollEvent( position: DpOffset, delta: DpOffset, event: UIEvent?, eventKind: TouchesEventKind ) { + mediaEnvironment.updatePointerPrecision(UiMediaScope.PointerPrecision.Fine) when (eventKind) { TouchesEventKind.BEGAN -> activitiesHandler.onActivitiesStarted() TouchesEventKind.MOVED -> {} @@ -515,11 +519,13 @@ internal class ComposeSceneMediator( ) } + @OptIn(ExperimentalMediaQueryApi::class) private fun onHoverEvent( position: DpOffset, event: UIEvent?, eventKind: TouchesEventKind ) { + mediaEnvironment.updatePointerPrecision(UiMediaScope.PointerPrecision.Fine) val eventType = when (eventKind) { TouchesEventKind.BEGAN -> PointerEventType.Enter TouchesEventKind.MOVED -> PointerEventType.Move @@ -558,6 +564,7 @@ internal class ComposeSceneMediator( * @param event the [UIEvent] associated with the touches * @param eventKind the [TouchesEventKind] of the touches */ + @OptIn(ExperimentalMediaQueryApi::class) private fun onTouchesEvent( touches: Set<*>, event: UIEvent?, @@ -569,6 +576,7 @@ internal class ComposeSceneMediator( TouchesEventKind.MOVED -> {} } + var anyIsStylus = false val pointers = touches.mapIndexed { index, touch -> touch as UITouch val position = touch.offsetInView(_backgroundView, screenDensity.density) @@ -578,6 +586,9 @@ internal class ComposeSceneMediator( UITouchTypePencil -> PointerType.Stylus else -> PointerType.Touch } + if (pointerType == PointerType.Stylus) { + anyIsStylus = true + } val id = touch.hashCode().toLong().takeIf { pointerType != PointerType.Mouse } ?: index.toLong() @@ -595,6 +606,12 @@ internal class ComposeSceneMediator( ) } + if (anyIsStylus) { + mediaEnvironment.updatePointerPrecision(UiMediaScope.PointerPrecision.Fine) + } else { + mediaEnvironment.updatePointerPrecision(UiMediaScope.PointerPrecision.Coarse) + } + // UIKit sends buttonMask that was before the release action. It should be empty if no // pressed pointers left. val pointerButtonsMask = event.buttonMaskOrZero.takeIf { @@ -878,6 +895,8 @@ internal class ComposeSceneMediator( private inner class PlatformContextImpl : PlatformContext { override val windowInfo: WindowInfo get() = windowContext.windowInfo + @OptIn(ExperimentalMediaQueryApi::class) + override val mediaEnvironment: UiMediaScope get() = this@ComposeSceneMediator.mediaEnvironment override val architectureComponentsOwner get() = this@ComposeSceneMediator.architectureComponentsOwner override val screenReader: PlatformScreenReader get() = platformScreenReader diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/UIKitComposeSceneLayer.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/UIKitComposeSceneLayer.ios.kt index 5293688a7595d..d6c6de02f3ac9 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/UIKitComposeSceneLayer.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/UIKitComposeSceneLayer.ios.kt @@ -19,7 +19,6 @@ package androidx.compose.ui.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.State import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint @@ -27,11 +26,12 @@ import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.navigationevent.UIKitNavigationEventInput +import androidx.compose.ui.platform.FrameRecomposer +import androidx.compose.ui.platform.MediaEnvironment import androidx.compose.ui.platform.FrameChoreographer import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.uikit.ComposeContainerConfiguration -import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalUIViewController import androidx.compose.ui.uikit.density import androidx.compose.ui.uikit.embedSubview @@ -60,7 +60,7 @@ internal class UIKitComposeSceneLayer( consumePointerInputOutside: Boolean = focusedViewsList != null, parentCoroutineContext: CoroutineContext, private val ownerProvider: PlatformArchitectureComponentsOwner, - private val interfaceOrientationState: State, + private val mediaEnvironment: MediaEnvironment, private var invalidateLayout: () -> Unit, private var invalidateDraw: () -> Unit, ) : ComposeSceneLayer { @@ -112,7 +112,7 @@ internal class UIKitComposeSceneLayer( coroutineContext = layerCoroutineContext, composeSceneFactory = ::createComposeScene, navigationEventInput = navigationEventInput, - interfaceOrientationState = interfaceOrientationState + mediaEnvironment = mediaEnvironment ).also { interactionView.embedSubview(it.backgroundView) it.isInterceptingOutsideEvents = consumePointerInputOutside 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..4c2876a9da5ac 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 @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMediaQueryApi::class) + package androidx.compose.ui.platform import androidx.compose.runtime.Composable @@ -25,7 +27,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 @@ -93,7 +97,8 @@ internal fun ProvidePlatformCompositionLocals( *values, LocalPlatformScreenReader provides platformContext.screenReader, LocalPlatformWindowInsets provides platformContext.windowInsets, - LocalPlatformPrefetchScheduler provides platformContext.prefetchScheduler, + LocalPlatformPrefetchScheduler providesComputed { 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..a16e764efc41a 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,8 @@ interface PlatformContext { */ val prefetchScheduler: PlatformPrefetchScheduler get() = NoOpPlatformPrefetchScheduler + val mediaEnvironment : UiMediaScope get() = NoOpMediaEnvironment + interface RootForTestListener { fun onRootForTestCreated(root: PlatformRootForTest) fun onRootForTestDisposed(root: PlatformRootForTest) @@ -389,3 +397,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 diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorTest.kt index 8dae6114316b1..8fbae2b388b60 100644 --- a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorTest.kt +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorTest.kt @@ -16,9 +16,9 @@ package androidx.compose.ui.integrations -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.navigationevent.UIKitNavigationEventInput import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner +import androidx.compose.ui.platform.MediaEnvironment import androidx.compose.ui.platform.FrameChoreographer import androidx.compose.ui.platform.PlatformWindowContext import androidx.compose.ui.platform.registerSkikoComposeImplementation @@ -27,7 +27,6 @@ import androidx.compose.ui.scene.ComposeSceneMediator import androidx.compose.ui.scene.PlatformLayersComposeScene import androidx.compose.ui.test.runUIKitInstrumentedTest import androidx.compose.ui.uikit.EndEdgePanGestureBehavior -import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.OnFocusBehavior import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset @@ -96,12 +95,14 @@ class ComposeSceneMediatorTest { coroutineContext: CoroutineContext, frameChoreographer: FrameChoreographer ): ComposeSceneMediator { + val windowContext = PlatformWindowContext() + val mediaEnvironment = MediaEnvironment(windowContext.windowInfo) val mediator = ComposeSceneMediator( frameChoreographer = frameChoreographer, onFocusBehavior = OnFocusBehavior.DoNothing, isClearFocusOnMouseDownEnabled = false, focusedViewsList = null, - windowContext = PlatformWindowContext(), + windowContext = windowContext, architectureComponentsOwner = DefaultArchitectureComponentsOwner(), coroutineContext = coroutineContext, navigationEventInput = UIKitNavigationEventInput( @@ -110,7 +111,7 @@ class ComposeSceneMediatorTest { getTopLeftOffsetInWindow = { IntOffset.Zero }, endEdgePanGestureBehavior = EndEdgePanGestureBehavior.Disabled, ), - interfaceOrientationState = mutableStateOf(InterfaceOrientation.Portrait), + mediaEnvironment = mediaEnvironment, composeSceneFactory = { platformContext -> registerSkikoComposeImplementation() PlatformLayersComposeScene( diff --git a/compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt b/compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt index b1bef5bc512f4..44a0fe3919cc9 100644 --- a/compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt +++ b/compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt @@ -21,19 +21,12 @@ package androidx.compose.ui.platform import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.text.AnnotatedString -import kotlin.getValue -import kotlin.js.Promise import kotlinx.coroutines.await -import org.w3c.files.Blob private val browserClipboard by lazy { getW3CClipboard() } -private val isSecureContext: Boolean by lazy { - isSecureContext() -} - // We don't expect the availability of browser APIs to change at runtime, so detect it and save // It's necessary for https://youtrack.jetbrains.com/issue/CMP-8631 private val isFullClipboardApiSupported: Boolean by lazy { diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt index ad328be79377f..e384bd5a5201b 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt @@ -40,7 +40,7 @@ import org.w3c.dom.ImageData import org.w3c.dom.HTMLElement import org.w3c.dom.Node -internal abstract class WebDragAndDropManager(private val rootNode: Node, eventListener: EventTargetListener, globalEventsListener: EventTargetListener, private val density: Density) : +internal abstract class WebDragAndDropManager(private val rootNode: Node, eventListener: EventTargetListener, globalEventsListener: EventTargetListener, private val densityProvider: () -> Density) : PlatformDragAndDropManager { override val isRequestDragAndDropTransferRequired: Boolean get() = false @@ -120,7 +120,7 @@ internal abstract class WebDragAndDropManager(private val rootNode: Node, eventL previousDragEventIsStart = true event as DragEvent - val scope = InternalStartTransferScope(density) + val scope = InternalStartTransferScope(densityProvider) if (scope.startTransfer(event)) { // without setting any kind of data in data transfer Safari on iOS won't proceed with drag action @@ -176,7 +176,7 @@ internal abstract class WebDragAndDropManager(private val rootNode: Node, eventL private val DragEvent.offset get() = Offset( x = offsetX.toFloat(), y = offsetY.toFloat() - ) * density.density + ) * densityProvider().density } @Suppress("UNUSED_PARAMETER") @@ -209,7 +209,7 @@ private fun IntArray.toUint8ClampedArray(): Uint8ClampedArray { } private class InternalStartTransferScope( - private val density: Density + private val densityProvider: () -> Density ) : PlatformDragAndDropSource.StartTransferScope { /** * Context for an ongoing drag session initiated from Compose. @@ -231,7 +231,7 @@ private class InternalStartTransferScope( val canvas = Canvas(imageBitmap) val canvasScope = CanvasDrawScope() - canvasScope.draw(density, LayoutDirection.Ltr, canvas, decorationSize, drawDragDecoration) + canvasScope.draw(densityProvider(), LayoutDirection.Ltr, canvas, decorationSize, drawDragDecoration) val intArray = IntArray(imageBitmap.width * imageBitmap.height) imageBitmap.readPixels(intArray) @@ -247,7 +247,7 @@ private class InternalStartTransferScope( canvasConverter.width = width canvasConverter.height = height - val scale = density.density + val scale = densityProvider().density require(scale > 0f) val widthNormalized = (width / scale).toInt() 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..feb627d82fecb 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 isVibrationApiSupported(): Boolean = js( //language=javascript """ typeof window !== 'undefined' && diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.web.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.web.kt index 7aa3e2628ba1e..a954230f33d07 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.web.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.web.kt @@ -44,6 +44,10 @@ internal fun isFullClipboardApiSupported(): Boolean = internal fun isSecureContext(): Boolean = js("window.isSecureContext === true") +internal val isSecureContext: Boolean by lazy { + isSecureContext() +} + internal fun isFallbackWriteTextApiAvailable(): Boolean = js("Boolean(window.navigator.clipboard && window.navigator.clipboard.writeText)") diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebMediaEnvironment.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebMediaEnvironment.kt new file mode 100644 index 0000000000000..a05f18cc7a563 --- /dev/null +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebMediaEnvironment.kt @@ -0,0 +1,291 @@ +/* + * 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.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +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.util.fastForEach +import androidx.compose.ui.window.MediaQueryListener +import androidx.compose.ui.window.MediaQueryStatus +import kotlin.js.Promise +import kotlinx.browser.window +import org.jetbrains.skiko.SystemTheme +import org.w3c.dom.events.Event +import org.w3c.dom.mediacapture.AUDIOINPUT +import org.w3c.dom.mediacapture.MediaDeviceInfo +import org.w3c.dom.mediacapture.MediaDeviceKind +import org.w3c.dom.mediacapture.VIDEOINPUT + +internal class WebMediaEnvironment( + val windowInfo: WindowInfo, + val onDensityChanged: (Density) -> Unit +) : UiMediaScope { + + private var isDisposed = false + + //Microphone And Camera detection Api supported + private val isMediaDevicesEnumerateSupported: Boolean = + isSecureContext && isMediaDevicesEnumerateSupported() + + // + + private val themeMediaQueryListener: MediaQueryListener = object : MediaQueryListener("(prefers-color-scheme: dark)") { + override fun onChange(matches: Boolean) { + _currentSystemTheme = if (matches) SystemTheme.DARK else SystemTheme.LIGHT + } + } + + private var _currentSystemTheme by mutableStateOf( + when(themeMediaQueryListener.matches()) { + MediaQueryStatus.MATCH -> SystemTheme.DARK + MediaQueryStatus.NO_MATCH -> SystemTheme.LIGHT + MediaQueryStatus.UNSUPPORTED -> SystemTheme.UNKNOWN + } + ) + + // + // + private var resolutionMediaQueryListener: MediaQueryListener? = null + private fun initializeResolutionMediaQuery() { + if (isDisposed) return + val contentScale = window.devicePixelRatio + resolutionMediaQueryListener?.dispose() + resolutionMediaQueryListener = object : MediaQueryListener("(resolution: ${contentScale}dppx)") { + override fun onChange(matches: Boolean) { + if (!isDisposed) { + if (!matches) { + val density = Density(window.devicePixelRatio.toFloat()) + onDensityChanged(density) + _systemDensity = density + } + initializeResolutionMediaQuery() + } + } + } + } + + // + // + private val orientationMediaQueryListener: MediaQueryListener = object : MediaQueryListener("(orientation: portrait)") { + override fun onChange(matches: Boolean) { + isOrientationPortrait = matches + } + } + + private var isOrientationPortrait by mutableStateOf(orientationMediaQueryListener.matches() == MediaQueryStatus.MATCH) + + // + // + private var _windowPosture by mutableStateOf(UiMediaScope.Posture.Flat) + + private val devicePostureEventCallback: (Event) -> Unit = { + _windowPosture = getDevicePosture() + } + + private fun getDevicePosture(): UiMediaScope.Posture { + val postureType = getDevicePostureType() + val isPortrait = isOrientationPortrait + return when (postureType) { + 1 -> if (isPortrait) UiMediaScope.Posture.Book else UiMediaScope.Posture.Tabletop + else -> UiMediaScope.Posture.Flat + } + + } + + private val isDevicePostureSupported = isDevicePostureApiSupported() + + // + private var _systemDensity by mutableStateOf(Density(window.devicePixelRatio.toFloat())) + + //TODO Hookup to virtualKeyboard.isShowing after https://github.com/JetBrains/compose-multiplatform-core/pull/3202 is merged + private var isImeVisible by mutableStateOf(false) + private var hasPhysicalKeyboard by mutableStateOf(true) + private var _pointerPrecision by mutableStateOf(UiMediaScope.PointerPrecision.None) + + // + private var _hasMicrophone by mutableStateOf(false) + + private var _hasCamera by mutableStateOf(false) + + private fun initializeMediaDevicesInfo() { + enumerateMediaDevices().then { extDevices -> + var hasMic = false + var hasCam = false + val devices = extDevices.toList() + devices.fastForEach { device -> + when (device.kind) { + MediaDeviceKind.AUDIOINPUT -> hasMic = true + MediaDeviceKind.VIDEOINPUT -> hasCam = true + } + } + _hasMicrophone = hasMic + _hasCamera = hasCam + extDevices + }.catch { + println("Failed to enumerate media devices: ${it.asJsException().message}") + null + } + } + + // + // + private val userAgent = window.navigator.userAgent + + private val tvAgentStrings = setOf( + "TV", "Chromecast", "Nexus Player", "Roku", "Tizen", "WebOS", + "Viera", "AFT", "BRAVIA", "TCL", "Hisense", "Xbox One", "PlayStation" + ) + + private val automotiveAgentStrings = setOf( + "Android Auto", "Apple CarPlay", "Tesla", "QtCarBrowser", "BMW", "Mercedes", "Audi" + ) + + private var _viewingDistance by mutableStateOf(getViewingDistance()) + + private fun getViewingDistance(): UiMediaScope.ViewingDistance { + return when { + automotiveAgentStrings.any(userAgent::contains) -> UiMediaScope.ViewingDistance.Medium + tvAgentStrings.any(userAgent::contains) -> UiMediaScope.ViewingDistance.Far + else -> UiMediaScope.ViewingDistance.Near + } + } + // + + val systemTheme: SystemTheme + get() = _currentSystemTheme + val systemDensity: Density + get() = _systemDensity + + override val windowPosture: UiMediaScope.Posture + get() = _windowPosture + override val windowWidth: Dp + get() = windowInfo.containerDpSize.width + override val windowHeight: Dp + get() = windowInfo.containerDpSize.height + override val pointerPrecision: UiMediaScope.PointerPrecision + get() = _pointerPrecision + override val keyboardKind: UiMediaScope.KeyboardKind + get() = + when { + hasPhysicalKeyboard -> UiMediaScope.KeyboardKind.Physical + isImeVisible -> UiMediaScope.KeyboardKind.Virtual + else -> UiMediaScope.KeyboardKind.None + } + override val hasMicrophone: Boolean + get() = _hasMicrophone + override val hasCamera: Boolean + get() = _hasCamera + override val viewingDistance: UiMediaScope.ViewingDistance + get() = _viewingDistance + + + init { + initializeResolutionMediaQuery() + if (isMediaDevicesEnumerateSupported) { + initializeMediaDevicesInfo() + if (isMediaDevicesChangeEventSupported()) { + window.navigator.mediaDevices.ondevicechange = { + initializeMediaDevicesInfo() + } + } + } + if (isDevicePostureSupported) { + addDevicePostureEventListener(devicePostureEventCallback) + _windowPosture = getDevicePosture() + } + } + + fun updateHardwareType( + keyboardKind: UiMediaScope.KeyboardKind, + pointerPrecision: UiMediaScope.PointerPrecision + ) { + val currentHasPhysicalKeyboard = hasPhysicalKeyboard + val currentPointerPrecision = _pointerPrecision + when (keyboardKind) { + UiMediaScope.KeyboardKind.Physical if !currentHasPhysicalKeyboard -> { + hasPhysicalKeyboard = true + } + + UiMediaScope.KeyboardKind.Virtual if currentHasPhysicalKeyboard -> { + hasPhysicalKeyboard = false + } + } + if (pointerPrecision != currentPointerPrecision) { + _pointerPrecision = pointerPrecision + } + } + + fun dispose() { + themeMediaQueryListener.dispose() + resolutionMediaQueryListener?.dispose() + orientationMediaQueryListener.dispose() + + if (isMediaDevicesEnumerateSupported && isMediaDevicesChangeEventSupported()) { + window.navigator.mediaDevices.ondevicechange = null + } + + if (isDevicePostureSupported) { + removeDevicePostureEventListener(devicePostureEventCallback) + } + } +} + + +//language=Js +private fun isMediaDevicesEnumerateSupported(): Boolean = + js("Boolean(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices)") + +//language=Js +private fun isMediaDevicesChangeEventSupported(): Boolean = + js("Boolean(navigator.mediaDevices.ondevicechange)") + +//language=Js +private fun enumerateMediaDevices(): Promise> = + js("navigator.mediaDevices.enumerateDevices()") + +//language=Js +private fun isDevicePostureApiSupported(): Boolean = js("Boolean(navigator.devicePosture)") + +// strings checks are faster on a JS side +// language=js +private fun getDevicePostureType(): Int = js( + """{ + switch (navigator.devicePosture.type) { + case 'continuous': + return 0; // UiMediaScope.Posture.Flat + case 'folded': + return 1; // UiMediaScope.Posture.Tabletop or UiMediaScope.Posture.Book + default: + return 0; // UiMediaScope.Posture.Flat + } + }""" +) + +//language=Js +private fun addDevicePostureEventListener(callback: (Event) -> Unit): Unit = + js("navigator.devicePosture.addEventListener('change', callback)") + +//language=Js +private fun removeDevicePostureEventListener(callback: (Event) -> Unit): Unit = + js("navigator.devicePosture.removeEventListener('change', callback)") \ No newline at end of file diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebPrefetchScheduler.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebPrefetchScheduler.kt index e8b6a0618734d..7b33f993a9051 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebPrefetchScheduler.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebPrefetchScheduler.kt @@ -127,12 +127,8 @@ private external interface IdleDeadline : JsAny { val didTimeout: Boolean } -internal val isIdleCallbackSupported: Boolean by lazy { - isIdleApiSupported() -} - @OptIn(ExperimentalWasmJsInterop::class) -private fun isIdleApiSupported(): Boolean = js("Boolean('requestIdleCallback' in window)") +internal fun isIdleCallbackApiSupported(): Boolean = js("Boolean('requestIdleCallback' in window)") @OptIn(ExperimentalWasmJsInterop::class) private fun requestIdleCallback(callback: (IdleDeadline) -> Unit): Int = diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebWindowInsetsManager.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebWindowInsetsManager.kt index 4a3dbb875feb6..d4b0374afb80a 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebWindowInsetsManager.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebWindowInsetsManager.kt @@ -68,10 +68,7 @@ private class WebWindowInsets( * zero; if it extends into the navigation bar area, the bottom inset will reflect the overlap. * */ -internal class WebWindowInsetsManager( - private val density: Density, - canvas: Element -) { +internal class WebWindowInsetsManager(canvas: Element) { private var canvasRect: DOMRect = canvas.getBoundingClientRect() set(value) { field = value @@ -79,6 +76,15 @@ internal class WebWindowInsetsManager( readAndUpdateIme() } + internal var density: Density = Density(window.devicePixelRatio.toFloat()) + set(value) { + if (field != value) { + field = value + readAndUpdateSafeArea() + readAndUpdateIme() + } + } + private val safeAreaInsets = mutableStateOf(PlatformInsets.Zero) private val imeInsets = mutableStateOf(PlatformInsets.Zero) diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt index 4685c415a7592..4b0b6c211797b 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt @@ -208,7 +208,7 @@ fun ComposeViewport( a11yContainerElement = a11yContainerElement, content = content, configuration = configuration, - state = DefaultWindowState(viewportContainer) + windowState = DefaultWindowState(viewportContainer) ) ComposeWindow.registerDisposableFor(positioningContainer) { 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 494c8ab0b2a9c..513894e569c28 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 @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:OptIn(ExperimentalWasmJsInterop::class) +@file:OptIn(ExperimentalWasmJsInterop::class, ExperimentalMediaQueryApi::class) package androidx.compose.ui.window @@ -23,11 +23,12 @@ import androidx.collection.mutableIntObjectMapOf import androidx.collection.mutableIntSetOf import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.LocalSystemTheme +import androidx.compose.ui.UiMediaScope import androidx.compose.ui.draganddrop.WebDragAndDropManager import androidx.compose.ui.events.EventTargetListener import androidx.compose.ui.geometry.Offset @@ -55,13 +56,18 @@ import androidx.compose.ui.internal.focusExt import androidx.compose.ui.navigationevent.BackNavigationEventInput import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner import androidx.compose.ui.platform.EmptyPlatformWindowInsets +import androidx.compose.ui.platform.FrameRecomposer import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformDragAndDropManager +import androidx.compose.ui.platform.PlatformOutOfFrameExecutor +import androidx.compose.ui.platform.PlatformPrefetchScheduler import androidx.compose.ui.platform.PlatformTextInputMethodRequest import androidx.compose.ui.platform.TextToolbar import androidx.compose.ui.platform.ViewConfiguration -import androidx.compose.ui.platform.WebOutOfFrameExecutor import androidx.compose.ui.platform.WebHapticFeedback +import androidx.compose.ui.platform.WebMediaEnvironment +import androidx.compose.ui.platform.WebOutOfFrameExecutor +import androidx.compose.ui.platform.WebPrefetchScheduler import androidx.compose.ui.platform.WebTextInputService import androidx.compose.ui.platform.WebTextToolbar import androidx.compose.ui.platform.WebWakeLockManager @@ -69,14 +75,11 @@ import androidx.compose.ui.platform.WebWindowInsetsManager import androidx.compose.ui.platform.WindowInfoImpl import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.platform.accessibility.ComposeWebSemanticsListener -import androidx.compose.ui.platform.isPostingTasksSupported import androidx.compose.ui.platform.installFallbackFontDownloader +import androidx.compose.ui.platform.isIdleCallbackApiSupported +import androidx.compose.ui.platform.isPostingTasksSupported +import androidx.compose.ui.platform.isVibrationApiSupported import androidx.compose.ui.scene.CanvasLayersComposeScene -import androidx.compose.ui.platform.FrameRecomposer -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.scene.ComposeSceneDragAndDropNode import androidx.compose.ui.scene.ComposeScenePointer import androidx.compose.ui.scene.PointerEventResult @@ -85,7 +88,6 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.size import androidx.compose.ui.unit.toDpRect @@ -104,11 +106,7 @@ import kotlin.js.js import kotlinx.browser.document import kotlinx.browser.window import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.receiveAsFlow import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkikoRenderDelegate import org.jetbrains.skiko.hostOs @@ -129,75 +127,12 @@ import org.w3c.dom.events.MouseEvent import org.w3c.dom.events.WheelEvent import org.w3c.dom.pointerevents.PointerEvent -private val actualDensity - get() = window.devicePixelRatio - -internal interface ComposeWindowState { - fun init() {} - fun sizeFlow(): Flow - - val globalEvents: EventTargetListener - - fun dispose() { - globalEvents.dispose() - } -} - -private sealed interface KeyboardModeState { - object Virtual : KeyboardModeState - object Hardware : KeyboardModeState -} - -internal class DefaultWindowState(private val viewportContainer: Element) : ComposeWindowState { - private val channel = Channel(CONFLATED) - - override val globalEvents = EventTargetListener(window) - - private var mediaQueryListener: MediaQueryListener? = null - - override fun init() { - - globalEvents.addDisposableEvent("resize") { - channel.trySend(getParentContainerBox()) - } - - recreateMediaQueryListener() - - channel.trySend(getParentContainerBox()) - } - - private fun getParentContainerBox(): IntSize { - return IntSize(viewportContainer.clientWidth, viewportContainer.clientHeight) - } - - private fun recreateMediaQueryListener() { - mediaQueryListener?.dispose() - val contentScale = actualDensity - mediaQueryListener = object : MediaQueryListener("(resolution: ${contentScale}dppx)") { - override fun onChange(matches: Boolean) { - if (!matches) { - channel.trySend(getParentContainerBox()) - } - recreateMediaQueryListener() - } - } - } - - override fun dispose() { - mediaQueryListener?.dispose() - super.dispose() - } - - override fun sizeFlow() = channel.receiveAsFlow() -} - @VisibleForTesting // This value is for internal usage, for example, to call ComposeWindow.dispose() in the tests internal val LocalComposeWindow: ProvidableCompositionLocal = staticCompositionLocalOf { error("ComposeWindow is not available in this composition") } -@OptIn(InternalComposeApi::class) internal class ComposeWindow( private val canvas: HTMLCanvasElement, private val rootNode: Node, @@ -206,21 +141,21 @@ internal class ComposeWindow( private val a11yContainerElement: HTMLDivElement?, private val configuration: ComposeViewportConfiguration, content: @Composable () -> Unit, - private val state: ComposeWindowState + private val windowState: ComposeWindowState ) { private var isDisposed = false private var actualActivePointerButtons: PointerButtons = PointerButtons() - private val density: Density = Density( - density = actualDensity.toFloat(), - fontScale = 1f - ) - private val _windowInfo = WindowInfoImpl().apply { isWindowFocused = true } + private val webMediaEnvironment : WebMediaEnvironment = WebMediaEnvironment(_windowInfo) { newDensity -> + scene.density = newDensity + insetsManager?.density = newDensity + } + @VisibleForTesting internal val archComponentsOwner = DefaultArchitectureComponentsOwner() @@ -234,12 +169,10 @@ internal class ComposeWindow( private var insetsManager: WebWindowInsetsManager? = null - private var keyboardModeState: KeyboardModeState = KeyboardModeState.Hardware - // Used in WebTextInputService. Also see https://youtrack.jetbrains.com/issue/CMP-8611 private var activeTouchOffset: Offset = Offset.Unspecified - private val clipTarget = clipTargetElement(canvas) + private val clipTarget by lazy(LazyThreadSafetyMode.NONE) { clipTargetElement(canvas) } // TODO: It must be shared between Compose instances. // It's supposed to be stored in platform's root view or window. @@ -259,11 +192,12 @@ internal class ComposeWindow( override val outOfFrameExecutor: PlatformOutOfFrameExecutor? get() = webOutOfFrameExecutor override val windowInfo get() = _windowInfo + override val mediaEnvironment get() = webMediaEnvironment override val architectureComponentsOwner get() = archComponentsOwner override val windowInsets get() = insetsManager?.windowInsets ?: EmptyPlatformWindowInsets override val dragAndDropManager: PlatformDragAndDropManager = object : - WebDragAndDropManager(rootNode, canvasEvents, state.globalEvents, density) { + WebDragAndDropManager(rootNode, canvasEvents, windowState.globalEvents, webMediaEnvironment::systemDensity) { override val rootDragAndDropNode: ComposeSceneDragAndDropNode get() = scene.rootDragAndDropNode } @@ -292,11 +226,12 @@ internal class ComposeWindow( } override val hapticFeedback by lazy(LazyThreadSafetyMode.NONE) { - WebHapticFeedback.webHapticFeedbackOrDefault() + if (isVibrationApiSupported()) WebHapticFeedback() else super.hapticFeedback } - override val prefetchScheduler: PlatformPrefetchScheduler = - if (isIdleCallbackSupported) WebPrefetchScheduler() else super.prefetchScheduler + override val prefetchScheduler: PlatformPrefetchScheduler by lazy(LazyThreadSafetyMode.NONE) { + if (isIdleCallbackApiSupported()) WebPrefetchScheduler() else super.prefetchScheduler + } override val semanticsOwnerListener: PlatformContext.SemanticsOwnerListener? = if (configuration.isA11YEnabled) { @@ -324,7 +259,7 @@ internal class ComposeWindow( get() = layerRoot override fun getNewGeometryForBackingInput(rect: Rect): DpRect { - val dpRect = rect.toDpRect(density) + val dpRect = rect.toDpRect(webMediaEnvironment.systemDensity) val left = dpRect.left.value val top = dpRect.top.value @@ -352,10 +287,10 @@ internal class ComposeWindow( object : ViewConfiguration by PlatformContext.DefaultViewConfiguration { // Aligning the touchSlop value with the Android default: // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewConfiguration.java?pli=1#191 - override val touchSlop: Float get() = with(density) { 8.dp.toPx() } + override val touchSlop: Float get() = with(webMediaEnvironment.systemDensity) { 8.dp.toPx() } override val maximumFlingVelocity: Float //https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/core/java/android/view/ViewConfiguration.java;l=240;drc=733537294b158d22f2ae383f2ed77c93741798e9 - get() = with(density) { 8000.dp.toPx() } + get() = with(webMediaEnvironment.systemDensity) { 8000.dp.toPx() } } override var isKeepScreenOnEnabled: Boolean @@ -394,15 +329,13 @@ internal class ComposeWindow( private val scene = CanvasLayersComposeScene( frameRecomposer = frameRecomposer, platformContext = platformContext, - density = density, + density = webMediaEnvironment.systemDensity, // TODO: Split layout invalidation from draw invalidation once the web host has distinct // scheduling paths for relayout vs redraw. invalidateLayout = sceneRenderingScope::onSceneInvalidation, invalidateDraw = sceneRenderingScope::onSceneInvalidation, ) - private val systemThemeObserver = getSystemThemeObserver() - private fun addTypedEvent( type: String, handler: (event: T) -> Unit @@ -465,7 +398,7 @@ internal class ComposeWindow( addTypedEvent(name, passive = false, handler = onPointerCallback) } - state.globalEvents.addDisposableEvent("dragend") { + windowState.globalEvents.addDisposableEvent("dragend") { // in Safari pointerup event is not firing when we drop or cancel drop // see https://youtrack.jetbrains.com/issue/CMP-10102 actualActivePointerButtons = PointerButtons() @@ -551,15 +484,15 @@ internal class ComposeWindow( canvasFocused = false } - state.globalEvents.addDisposableEvent("focus") { + windowState.globalEvents.addDisposableEvent("focus") { archComponentsOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) } - state.globalEvents.addDisposableEvent("blur") { + windowState.globalEvents.addDisposableEvent("blur") { archComponentsOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) } - state.globalEvents.addDisposableEvent("visibilitychange") { event -> + windowState.globalEvents.addDisposableEvent("visibilitychange") { event -> archComponentsOwner.lifecycle.handleLifecycleEvent( if (documentIsVisible()) Lifecycle.Event.ON_START else Lifecycle.Event.ON_STOP @@ -570,14 +503,16 @@ internal class ComposeWindow( init { if (configuration.enableBrowserWindowInsets) { checkViewportFitCover() - insetsManager = WebWindowInsetsManager(density, canvas) + insetsManager = WebWindowInsetsManager(canvas).apply { + density = webMediaEnvironment.systemDensity + } } initEvents(canvas) - state.init() + windowState.init() - scene.density = density + scene.density = webMediaEnvironment.systemDensity archComponentsOwner.enableSavedStateHandles() val interopContainer = WebInteropContainer(InteropViewGroup(interopContainerElement)) @@ -588,7 +523,7 @@ internal class ComposeWindow( } scene.setContent { CompositionLocalProvider( - LocalSystemTheme provides systemThemeObserver.currentSystemTheme.value, + LocalSystemTheme provides webMediaEnvironment.systemTheme, LocalInteropContainer provides interopContainer, LocalActiveClipEventsTarget provides clipEventsTargetProvider, LocalComposeWindow provides this, @@ -601,7 +536,7 @@ internal class ComposeWindow( } LaunchedEffect(Unit) { - state.sizeFlow().collect { size -> + windowState.sizeFlow().collect { size -> // Convert to proper type: IntSize was exposed to public API with meaning of DPs. val boxSize = DpSize(size.width.dp, size.height.dp) this@ComposeWindow.resize(boxSize) @@ -632,7 +567,7 @@ internal class ComposeWindow( } private fun resize(boxSize: DpSize) { - val sizeInPx = boxSize.toSize(density).toIntSize() + val sizeInPx = boxSize.toSize(webMediaEnvironment.systemDensity).toIntSize() // we need to scale canvas both via CSS styling and HTML attributes // https://www.khronos.org/webgl/wiki/HandlingHighDPI @@ -672,8 +607,9 @@ internal class ComposeWindow( skiaLayer.detach() insetsManager?.dispose() - systemThemeObserver.dispose() - state.dispose() + webMediaEnvironment.dispose() + windowState.dispose() + // modern browsers supposed to garbage collect all events on the element disposed // but actually we never can be sure dom element was collected in first place canvasEvents.dispose() @@ -684,7 +620,7 @@ internal class ComposeWindow( val event: PointerEvent, val containerOffset: Offset ) { - val composePointer = event.toScenePointerEvent(containerOffset, density) + val composePointer = event.toScenePointerEvent(containerOffset, webMediaEnvironment.systemDensity) override fun equals(other: Any?): Boolean { if (this === other) return true @@ -728,8 +664,30 @@ internal class ComposeWindow( } private fun onPointerEvent(event: PointerEvent) { + val pointerType = event.getPointerType() + when(pointerType) { + PointerType.Mouse -> { + webMediaEnvironment.updateHardwareType( + UiMediaScope.KeyboardKind.Physical, + UiMediaScope.PointerPrecision.Fine + ) + } + PointerType.Stylus -> { + webMediaEnvironment.updateHardwareType( + UiMediaScope.KeyboardKind.Virtual, + UiMediaScope.PointerPrecision.Fine + ) + } + PointerType.Touch -> { + webMediaEnvironment.updateHardwareType( + UiMediaScope.KeyboardKind.Virtual, + UiMediaScope.PointerPrecision.Coarse + ) + } + } + if (event.type == "pointercancel") { - if (isTouchEvent(event)) { + if (pointerType == PointerType.Touch) { activeTouchPointers.clear() activeTouchPointersConsumedMoves.remove(event.pointerId) activeTouchOffset = Offset.Unspecified @@ -748,9 +706,7 @@ internal class ComposeWindow( var result: PointerEventResult? = null var anyChangeConsumed = false - if (isMouseEvent(event)) { - keyboardModeState = KeyboardModeState.Hardware - + if (pointerType == PointerType.Mouse) { // Track active mouse buttons. Used as a fallback for unreliable // `buttons` in wheel events (see CMP-9900) and to reset state on // `dragend` in Safari (see CMP-10102). @@ -768,6 +724,7 @@ internal class ComposeWindow( position = event.offset, timeMillis = event.timeStamp.toInt().toLong(), buttons = event.composeButtons, + type = pointerType, keyboardModifiers = PointerKeyboardModifiers( isCtrlPressed = event.ctrlKey, isMetaPressed = event.metaKey, @@ -797,8 +754,6 @@ internal class ComposeWindow( if (inputModeManager.inputMode != InputMode.Touch) { inputModeManager.requestInputMode(InputMode.Touch) } - keyboardModeState = KeyboardModeState.Virtual - val current: TouchEventWithContainerOffset val active = activeTouchPointers[event.pointerId] if (active == null) { @@ -833,11 +788,16 @@ internal class ComposeWindow( } coalescedEvents.fastForEach { coalescedEvent -> - val coalescedEventType = coalescedEvent.getPointerEventType() - val sceneEvent = coalescedEvent.toScenePointerEvent(current.containerOffset, density) + val coalescedPointerEventType = coalescedEvent.getPointerEventType() + val coalescedPointerType = coalescedEvent.getPointerType() + val sceneEvent = coalescedEvent.toScenePointerEvent( + current.containerOffset, + webMediaEnvironment.systemDensity, + coalescedPointerType + ) pointers[indexOfCurrentPointer] = sceneEvent result = scene.sendPointerEvent( - eventType = coalescedEventType, + eventType = coalescedPointerEventType, pointers = pointers, buttons = buttons, keyboardModifiers = keyboardModifiers, @@ -884,7 +844,11 @@ internal class ComposeWindow( private fun onWheelEvent( event: WheelEvent, ) { - keyboardModeState = KeyboardModeState.Hardware + //Inferred PointerPrecision.Fine for wheel-based events + webMediaEnvironment.updateHardwareType( + UiMediaScope.KeyboardKind.Physical, + UiMediaScope.PointerPrecision.Fine + ) // Shift + mouse wheel means horizontal scroll. Some browsers swap the axes // for us (report deltaX instead of deltaY), some don't. @@ -928,11 +892,14 @@ internal class ComposeWindow( } } - private val MouseEvent.offset - get() = Offset( - x = offsetX.toFloat() * density.density, - y = offsetY.toFloat() * density.density - ) + private val MouseEvent.offset : Offset + get() { + val density = webMediaEnvironment.systemDensity + return Offset( + x = offsetX.toFloat() * density.density, + y = offsetY.toFloat() * density.density + ) + } companion object { private val DomDisposableRegistry = WeakMap() @@ -1057,14 +1024,6 @@ private fun checkViewportFitCover(): Unit = js( })()""" ) -// strings checks are faster on a JS side -// language=js -private fun isTouchEvent(event: PointerEvent): Boolean = js("event.pointerType === 'touch'") - -// strings checks are faster on a JS side -// language=js -private fun isMouseEvent(event: PointerEvent): Boolean = js("event.pointerType === 'mouse'") - // strings checks are faster on a JS side // language=js private fun getPointerEventCode(event: PointerEvent): Int = js( @@ -1096,6 +1055,30 @@ private fun PointerEvent.getPointerEventType(): PointerEventType = else -> PointerEventType.Unknown } +// strings checks are faster on a JS side +// language=js +private fun getPointerTypeCode(event: PointerEvent): Int = js( + """{ + switch (event.pointerType) { + case 'touch': + return 1; // PointerType.Touch + case 'mouse': + return 2; // PointerType.Mouse + case 'pen': + return 3; // PointerType.Stylus + default: + return 0; // PointerType.Unknown + } + }""" +) +private fun PointerEvent.getPointerType(): PointerType = + when (getPointerTypeCode(this)) { + 1 -> PointerType.Touch + 2 -> PointerType.Mouse + 3 -> PointerType.Stylus + else -> PointerType.Unknown + } + private fun Element.isFocused(): Boolean { val activeElement = when { document.activeElement?.shadowRoot != null -> (document.activeElement?.shadowRoot as? ShadowRootExt)?.activeElement diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowState.web.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowState.web.kt new file mode 100644 index 0000000000000..1134e2ada087d --- /dev/null +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowState.web.kt @@ -0,0 +1,57 @@ +/* + * 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. + */ + +package androidx.compose.ui.window + +import androidx.compose.ui.events.EventTargetListener +import androidx.compose.ui.unit.IntSize +import kotlinx.browser.window +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import org.w3c.dom.Element + +internal interface ComposeWindowState { + fun init() {} + fun sizeFlow(): Flow + + val globalEvents: EventTargetListener + + fun dispose() { + globalEvents.dispose() + } +} + +internal class DefaultWindowState(private val viewportContainer: Element) : ComposeWindowState { + private val channel = Channel(Channel.CONFLATED) + + override val globalEvents = EventTargetListener(window) + + override fun init() { + + globalEvents.addDisposableEvent("resize") { + channel.trySend(getParentContainerBox()) + } + + channel.trySend(getParentContainerBox()) + } + + private fun getParentContainerBox(): IntSize { + return IntSize(viewportContainer.clientWidth, viewportContainer.clientHeight) + } + + override fun sizeFlow() = channel.receiveAsFlow() +} \ No newline at end of file diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt deleted file mode 100644 index 7e6479dbe59b0..0000000000000 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023 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.window - -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import org.jetbrains.skiko.SystemTheme - -internal interface SystemThemeObserver { - val currentSystemTheme: State - - fun dispose() -} - -private class SystemThemeObserverImpl : SystemThemeObserver { - override val currentSystemTheme: State - get() = _currentSystemTheme - - private val mediaQueryListener: MediaQueryListener = object : MediaQueryListener("(prefers-color-scheme: dark)") { - override fun onChange(matches: Boolean) { - _currentSystemTheme.value = if (matches) SystemTheme.DARK else SystemTheme.LIGHT - } - } - - private val _currentSystemTheme = mutableStateOf( - when(mediaQueryListener.matches()) { - MediaQueryStatus.MATCH -> SystemTheme.DARK - MediaQueryStatus.NO_MATCH -> SystemTheme.LIGHT - MediaQueryStatus.UNSUPPORTED -> SystemTheme.UNKNOWN - } - ) - - override fun dispose() { - mediaQueryListener.dispose() - } -} - -internal fun getSystemThemeObserver(): SystemThemeObserver = SystemThemeObserverImpl() diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/ComposeWindowLifecycleTest.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/ComposeWindowLifecycleTest.kt index 277fb5263db4d..16ec4175a1159 100644 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/ComposeWindowLifecycleTest.kt +++ b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/ComposeWindowLifecycleTest.kt @@ -47,7 +47,7 @@ class ComposeWindowLifecycleTest : OnCanvasTests { a11yContainerElement = null, content = {}, configuration = ComposeViewportConfiguration(), - state = DefaultWindowState(document.documentElement!!) + windowState = DefaultWindowState(document.documentElement!!) ) val eventsChannel = Channel(10)