Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp interop API to align it with Android and make it reusable #1489

Merged
merged 72 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
32d9cc0
Add changes
elijah-semyonov Jul 31, 2024
5394dea
Prepare stubs for integrating with iOS
elijah-semyonov Aug 1, 2024
6072c9a
Add a couple of changes
elijah-semyonov Aug 1, 2024
7b2e377
Fix compilation bug
elijah-semyonov Aug 1, 2024
3358b7d
Wire (almost) iOS implementation
elijah-semyonov Aug 1, 2024
be52170
Wire (almost) iOS implementation
elijah-semyonov Aug 1, 2024
904992a
Wire (almost) iOS implementation
elijah-semyonov Aug 1, 2024
95ac1a9
Wire (almost) iOS implementation
elijah-semyonov Aug 2, 2024
968f265
Initial wiring for Swing
elijah-semyonov Aug 2, 2024
2bd0804
Roll back stuff
elijah-semyonov Aug 2, 2024
f211306
Wire UIKitView
elijah-semyonov Aug 2, 2024
fab364c
Wire UIKitViewController
elijah-semyonov Aug 5, 2024
d31fe2a
Fix desktop red code
elijah-semyonov Aug 5, 2024
3d5a324
Refactor
elijah-semyonov Aug 5, 2024
ab03781
Refactor
elijah-semyonov Aug 5, 2024
5dbb74c
Make blue thing not interactive
elijah-semyonov Aug 5, 2024
212cb1e
Add swing integration
elijah-semyonov Aug 5, 2024
fa5ae87
Fix stuff
elijah-semyonov Aug 5, 2024
5368451
Fix incorrect containment
elijah-semyonov Aug 5, 2024
7d64f32
Modify comment
elijah-semyonov Aug 5, 2024
295c256
Move InteropViewHolder to a separate file
elijah-semyonov Aug 5, 2024
dc64828
Remove asAwtComponent
elijah-semyonov Aug 6, 2024
d7ff9bc
Change AbstractInvocationError to function call
elijah-semyonov Aug 6, 2024
586ce1f
Rework isAttachedToWindow
elijah-semyonov Aug 6, 2024
e4c6b80
Rename interopWrappingView to findAncestorInteropWrappingView
elijah-semyonov Aug 6, 2024
4dd2859
Refactor insertion into hierarchy
elijah-semyonov Aug 6, 2024
cf89a20
Remove imports
elijah-semyonov Aug 6, 2024
b4ae9ca
Add comments
elijah-semyonov Aug 6, 2024
31590b7
Rename and add comments
elijah-semyonov Aug 6, 2024
17ae02e
Add comment, make interop example more verbose
elijah-semyonov Aug 6, 2024
f876ef6
Modify comment
elijah-semyonov Aug 6, 2024
2054a5e
Fix runUpdate ordering bug
elijah-semyonov Aug 6, 2024
19b8efc
Fix adding component instead of group
elijah-semyonov Aug 6, 2024
817eed9
Remove coma
elijah-semyonov Aug 6, 2024
092b385
Fix order
elijah-semyonov Aug 6, 2024
a1115f2
Refactor update call
elijah-semyonov Aug 6, 2024
6dc56d9
Refactor update call
elijah-semyonov Aug 6, 2024
6bca93c
WIP: revert to state with passing tests
elijah-semyonov Aug 6, 2024
af40fbc
WIP: revert to state with passing tests
elijah-semyonov Aug 6, 2024
8d02b7a
Remove redundant imports
elijah-semyonov Aug 6, 2024
809d3bb
Remove temp backup
elijah-semyonov Aug 6, 2024
9cf2ea9
Hoist focusListener inside holder
elijah-semyonov Aug 6, 2024
22ff756
Add TODO
elijah-semyonov Aug 6, 2024
44d8586
Change emitted tree layout for FocusSwitcher
elijah-semyonov Aug 6, 2024
f6e7a3b
Hoist focus switcher inside SwingInteropViewHolder
elijah-semyonov Aug 6, 2024
a2a3b4b
Wire rest of old API to UIKit
elijah-semyonov Aug 6, 2024
5e05bca
Deprecate
elijah-semyonov Aug 6, 2024
213c201
Move
elijah-semyonov Aug 6, 2024
b5f7c29
Wrap up
elijah-semyonov Aug 6, 2024
2679e30
Move Unspecified check to toUIColor()
elijah-semyonov Aug 6, 2024
7d29841
Move focus switcher to a separate file
elijah-semyonov Aug 7, 2024
003f6c7
Refactor
elijah-semyonov Aug 7, 2024
13eeea7
Revert comment
elijah-semyonov Aug 7, 2024
14dacf5
Update compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/viewi…
elijah-semyonov Aug 7, 2024
82a6c79
Update doc
elijah-semyonov Aug 7, 2024
9549221
Rename
elijah-semyonov Aug 7, 2024
ac3f7f3
Add comment
elijah-semyonov Aug 7, 2024
4015974
Detach view controller from mediator.
elijah-semyonov Aug 7, 2024
d6365d0
Make bounds change synchronous
elijah-semyonov Aug 7, 2024
97d6d72
Fix formatting
elijah-semyonov Aug 7, 2024
1a60479
Make name more meaningful, add comments
elijah-semyonov Aug 7, 2024
7b32436
Add TODO
elijah-semyonov Aug 7, 2024
ea24b79
Separate concerns of `update` and `onInteropViewLayoutChange`
elijah-semyonov Aug 7, 2024
35daf0e
Rename FocusSwitcher to InteropFocusSwitcher
elijah-semyonov Aug 7, 2024
75a5778
Remove invokeLater
elijah-semyonov Aug 7, 2024
a06a6a1
Adopt update scheduling strategy on Swing
elijah-semyonov Aug 8, 2024
9d3e630
Rearrange update execution on Swing.
elijah-semyonov Aug 9, 2024
2e64754
Rename
elijah-semyonov Aug 9, 2024
0d885d3
Remove ScheduledUpdate
elijah-semyonov Aug 12, 2024
829468d
Add TODO about owner merge
elijah-semyonov Aug 12, 2024
567f8e7
Add missing TODO
elijah-semyonov Aug 12, 2024
2b0a312
Add TODO: Align "platform" vs "native" naming
elijah-semyonov Aug 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.interop.UIKitView
import androidx.compose.ui.interop.UIKitViewController
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp
import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.objcPtr
import kotlinx.cinterop.readValue
import platform.CoreGraphics.CGRectMake
import platform.CoreGraphics.CGRectZero
Expand Down Expand Up @@ -71,24 +75,37 @@ private class TouchReactingView: UIView(frame = CGRectZero.readValue()) {

val UIKitInteropExample = Screen.Example("UIKitInterop") {
var text by remember { mutableStateOf("Type something") }
var updatedValue by remember { mutableStateOf(null as Offset?) }

LazyColumn(Modifier.fillMaxSize()) {
item {
UIKitView(
factory = {
MKMapView()
},
modifier = Modifier.fillMaxWidth().height(200.dp)
modifier = Modifier.fillMaxWidth().height(200.dp),
update = {
println("MKMapView updated")
}
)
}

item {
UIKitViewController(
factory = {
object : UIViewController(nibName = null, bundle = null) {
val label = UILabel()

override fun loadView() {
setView(label)
}

override fun viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = UIColor.blueColor
label.textAlignment = NSTextAlignmentCenter
label.textColor = UIColor.whiteColor
label.backgroundColor = UIColor.blueColor
}

override fun viewWillAppear(animated: Boolean) {
Expand Down Expand Up @@ -116,7 +133,20 @@ val UIKitInteropExample = Screen.Example("UIKitInterop") {
}
}
},
modifier = Modifier.fillMaxWidth().height(100.dp),
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.onGloballyPositioned { coordinates ->
val rootCoordinates = coordinates.findRootCoordinates()
val box = coordinates.localBoundingBoxOf(rootCoordinates, clipBounds = false)
updatedValue = box.topLeft
},
update = { viewController ->
updatedValue?.let {
viewController.label.text = "${it.x}, ${it.y}"
}
},
interactive = false
)
}
items(100) { index ->
Expand All @@ -136,7 +166,7 @@ val UIKitInteropExample = Screen.Example("UIKitInterop") {
3 -> ComposeUITextField(text, onValueChange = { text = it }, Modifier.fillMaxWidth().height(40.dp))
4 -> UIKitView(
factory = { TouchReactingView() },
modifier = Modifier.fillMaxWidth().height(40.dp)
modifier = Modifier.fillMaxWidth().height(40.dp),
)
}
}
Expand Down Expand Up @@ -171,6 +201,7 @@ private fun ComposeUITextField(value: String, onValueChange: (String) -> Unit, m
},
modifier = modifier,
update = { textField ->
println("Update called for UITextField(0x${textField.objcPtr().toLong().toString(16)}, value = $value")
textField.text = value
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2024 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.awt

import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.viewinterop.InteropViewGroup
import java.awt.event.FocusEvent

internal class InteropFocusSwitcher(
private val group: InteropViewGroup,
private val focusManager: FocusManager,
) {
private val backwardTracker = Tracker {
val component = group.focusTraversalPolicy.getFirstComponent(group)
if (component != null) {
component.requestFocus(FocusEvent.Cause.TRAVERSAL_FORWARD)
} else {
moveForward()
}
}

private val forwardTracker = Tracker {
val component = group.focusTraversalPolicy.getLastComponent(group)
if (component != null) {
component.requestFocus(FocusEvent.Cause.TRAVERSAL_BACKWARD)
} else {
moveBackward()
}
}

val backwardTrackerModifier: Modifier
get() = backwardTracker.modifier

val forwardTrackerModifier: Modifier
get() = forwardTracker.modifier

fun moveBackward() {
backwardTracker.requestFocusWithoutEvent()
focusManager.moveFocus(FocusDirection.Previous)
}

fun moveForward() {
forwardTracker.requestFocusWithoutEvent()
focusManager.moveFocus(FocusDirection.Next)
}

/**
* A helper class that can help:
* - to prevent recursive focus events
* (a case when we focus the same element inside `onFocusEvent`)
* - to prevent triggering `onFocusEvent` while requesting focus somewhere else
*/
private class Tracker(
private val onNonRecursiveFocused: () -> Unit
) {
private val requester = FocusRequester()

private var isRequestingFocus = false
private var isHandlingFocus = false

fun requestFocusWithoutEvent() {
try {
isRequestingFocus = true
requester.requestFocus()
} finally {
isRequestingFocus = false
}
}

val modifier = Modifier
.focusRequester(requester)
.onFocusEvent {
if (!isRequestingFocus && !isHandlingFocus && it.isFocused) {
try {
isHandlingFocus = true
onNonRecursiveFocused()
} finally {
isHandlingFocus = false
}
}
}
.focusTarget()
}
}
Loading