Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions compose/ui/ui-backhandler/api/ui-backhandler.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@

// Library unique name: <org.jetbrains.compose.ui:ui-backhandler>
final val androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_BackEventCompat$stableprop // androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_BackEventCompat$stableprop|#static{}androidx_compose_ui_backhandler_BackEventCompat$stableprop[0]
final val androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_BackEventHandler$stableprop // androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_BackEventHandler$stableprop|#static{}androidx_compose_ui_backhandler_BackEventHandler$stableprop[0]
final val androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_ProgressBackEventHandler$stableprop // androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_ProgressBackEventHandler$stableprop|#static{}androidx_compose_ui_backhandler_ProgressBackEventHandler$stableprop[0]

final fun androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_BackEventCompat$stableprop_getter(): kotlin/Int // androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_BackEventCompat$stableprop_getter|androidx_compose_ui_backhandler_BackEventCompat$stableprop_getter(){}[0]
final fun androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_BackEventHandler$stableprop_getter(): kotlin/Int // androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_BackEventHandler$stableprop_getter|androidx_compose_ui_backhandler_BackEventHandler$stableprop_getter(){}[0]
final fun androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_ProgressBackEventHandler$stableprop_getter(): kotlin/Int // androidx.compose.ui.backhandler/androidx_compose_ui_backhandler_ProgressBackEventHandler$stableprop_getter|androidx_compose_ui_backhandler_ProgressBackEventHandler$stableprop_getter(){}[0]
3 changes: 2 additions & 1 deletion compose/ui/ui-backhandler/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ kotlin {
jbMain {
dependsOn(commonMain)
dependencies {
implementation(project(":navigationevent:navigationevent-compose"))
def navigationEventVersion = project.findProperty('artifactRedirection.version.androidx.navigationevent')
implementation("androidx.navigationevent:navigationevent:$navigationEventVersion")
}
}
desktopMain.dependsOn(jbMain)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package androidx.compose.ui.backhandler

import androidx.navigationevent.NavigationEventHandler
import androidx.navigationevent.NavigationEventInfo

internal class BackEventHandler(
enabled: Boolean,
private val onBack: () -> Unit
) : NavigationEventHandler<NavigationEventInfo.None>(
initialInfo = NavigationEventInfo.None,
isBackEnabled = enabled,
) {
override fun onBackCompleted() {
onBack()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,95 +20,58 @@ package androidx.compose.ui.backhandler

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.navigationevent.NavigationEvent
import androidx.navigationevent.NavigationEventInfo
import androidx.navigationevent.NavigationEventTransitionState
import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner
import androidx.navigationevent.compose.NavigationBackHandler
import androidx.navigationevent.compose.rememberNavigationEventState
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.Channel
import androidx.compose.ui.InternalComposeUiApi
import androidx.navigationevent.NavigationEventDispatcherOwner
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch

@InternalComposeUiApi
val LocalCompatNavigationEventDispatcherOwner =
staticCompositionLocalOf<NavigationEventDispatcherOwner?> { null }

@OptIn(InternalComposeUiApi::class)
@Deprecated("Use NavigationEventHandler instead")
@ExperimentalComposeUiApi
@Composable
actual fun PredictiveBackHandler(
enabled: Boolean,
onBack: suspend (progress: Flow<BackEventCompat>) -> Unit
) {
val owner = LocalCompatNavigationEventDispatcherOwner.current ?: error(
"No NavigationEventDispatcher was provided via LocalCompatNavigationEventDispatcherOwner"
)
val dispatcher = owner.navigationEventDispatcher
val coroutineScope = rememberCoroutineScope()
val navEventState = rememberNavigationEventState(NavigationEventInfo.None)

var progressChannel: Channel<BackEventCompat>? by remember(onBack) {
mutableStateOf(null)
val handler = remember(onBack) {
ProgressBackEventHandler(enabled, onBack, coroutineScope)
}
handler.isBackEnabled = enabled

fun getActiveProgressChannel(): Channel<BackEventCompat> {
val currentProgressChannel = progressChannel
if (currentProgressChannel == null) {
val progress = Channel<BackEventCompat>()
progressChannel = progress
coroutineScope.launch {
onBack(progress.consumeAsFlow())
}
return progress
} else {
return currentProgressChannel
}
}

val transitionState = navEventState.transitionState
if (transitionState is NavigationEventTransitionState.InProgress) {
LaunchedEffect(transitionState) {
val navEvent = transitionState.latestEvent
val swipeEdge = when (navEvent.swipeEdge) {
NavigationEvent.EDGE_RIGHT -> BackEventCompat.EDGE_RIGHT
else -> BackEventCompat.EDGE_LEFT
}
val event = BackEventCompat(
navEvent.touchX, navEvent.touchY, navEvent.progress, swipeEdge
)
getActiveProgressChannel().send(event)
}
}

NavigationBackHandler(
state = navEventState,
isBackEnabled = enabled,
onBackCancelled = {
getActiveProgressChannel().close(CancellationException("Cancelled"))
progressChannel = null
},
onBackCompleted = {
getActiveProgressChannel().close()
progressChannel = null
}
)
DisposableEffect(Unit) {
onDispose {
progressChannel?.close(CancellationException("Disposed"))
progressChannel = null
}
DisposableEffect(dispatcher, handler) {
dispatcher.addHandler(handler)
onDispose { handler.remove() }
}
}

@OptIn(InternalComposeUiApi::class)
@Deprecated("Use NavigationEventHandler instead")
@ExperimentalComposeUiApi
@Composable
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
NavigationBackHandler(
state = rememberNavigationEventState(NavigationEventInfo.None),
isBackEnabled = enabled,
onBackCompleted = onBack
val owner = LocalCompatNavigationEventDispatcherOwner.current ?: error(
"No NavigationEventDispatcher was provided via LocalCompatNavigationEventDispatcherOwner"
)
val dispatcher = owner.navigationEventDispatcher
val handler = remember(onBack) {
BackEventHandler(enabled, onBack)
}
handler.isBackEnabled = enabled

DisposableEffect(dispatcher, handler) {
dispatcher.addHandler(handler)
onDispose { handler.remove() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package androidx.compose.ui.backhandler

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.navigationevent.NavigationEvent
import androidx.navigationevent.NavigationEventHandler
import androidx.navigationevent.NavigationEventInfo
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch

@OptIn(ExperimentalComposeUiApi::class)
internal class ProgressBackEventHandler(
enabled: Boolean,
private val onBack: suspend (progress: Flow<BackEventCompat>) -> Unit,
private val coroutineScope: CoroutineScope
) : NavigationEventHandler<NavigationEventInfo.None>(
initialInfo = NavigationEventInfo.None,
isBackEnabled = enabled,
) {
private var progressChannel: Channel<BackEventCompat>? = null

override fun onBackStarted(event: NavigationEvent) {
progressChannel?.close(CancellationException("Disposed"))
progressChannel = Channel<BackEventCompat>().also { channel ->
coroutineScope.launch {
onBack(channel.consumeAsFlow())
}
}
}

override fun onBackProgressed(event: NavigationEvent) {
progressChannel?.let { channel ->
val swipeEdge = when (event.swipeEdge) {
NavigationEvent.EDGE_RIGHT -> BackEventCompat.EDGE_RIGHT
else -> BackEventCompat.EDGE_LEFT
}
val event = BackEventCompat(
event.touchX, event.touchY, event.progress, swipeEdge
)
coroutineScope.launch {
channel.send(event)
}
}
}

override fun onBackCancelled() {
progressChannel?.close(CancellationException("Cancelled"))
progressChannel = null
}

override fun onBackCompleted() {
progressChannel?.close()
progressChannel = null
}
}
1 change: 1 addition & 0 deletions compose/ui/ui-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
implementation(libs.atomicFu)
implementation(project(":lifecycle:lifecycle-runtime-compose"))
implementation(project(":navigationevent:navigationevent-compose"))
implementation(project(":compose:ui:ui-backhandler"))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
import androidx.compose.ui.draganddrop.DragAndDropTransferData
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
Expand Down Expand Up @@ -543,6 +544,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
CompositionLocalProvider(
LocalLifecycleOwner provides testOwner,
LocalNavigationEventDispatcherOwner provides testOwner,
LocalCompatNavigationEventDispatcherOwner provides testOwner,
content = content,
)
}
Expand Down
1 change: 1 addition & 0 deletions compose/ui/ui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
api(project(":compose:ui:ui-text"))
api(libs.skikoCommon)
implementation(libs.atomicFu)
implementation(project(":compose:ui:ui-backhandler")) //https://youtrack.jetbrains.com/issue/CMP-9008
def navigationEventVersion = project.findProperty('artifactRedirection.version.androidx.navigationevent')
implementation("androidx.navigationevent:navigationevent:$navigationEventVersion")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.ui.awt.AwtEventFilter
import androidx.compose.ui.awt.AwtEventListener
import androidx.compose.ui.awt.AwtEventListeners
import androidx.compose.ui.awt.RenderSettings
import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.navigationevent.DesktopNavigationEventInput
import androidx.compose.ui.platform.DisposableSaveableStateRegistry
Expand Down Expand Up @@ -589,6 +590,7 @@ private fun ProvideContainerCompositionLocals(
LocalSaveableStateRegistry provides saveableStateRegistry,
LocalInternalViewModelStoreOwner provides composeContainer,
LocalInternalNavigationEventDispatcherOwner provides composeContainer,
LocalCompatNavigationEventDispatcherOwner provides composeContainer,
content = content,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
import androidx.compose.runtime.saveable.SaveableStateRegistry
import androidx.compose.ui.LocalSystemTheme
import androidx.compose.ui.SystemTheme
import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.hapticfeedback.CupertinoHapticFeedback
import androidx.compose.ui.navigationevent.UIKitNavigationEventInput
Expand Down Expand Up @@ -513,6 +514,7 @@ internal class ComposeHostingViewController(
LocalLifecycleOwner provides archComponentsOwner,
LocalInternalViewModelStoreOwner provides archComponentsOwner,
LocalInternalNavigationEventDispatcherOwner provides archComponentsOwner,
LocalCompatNavigationEventDispatcherOwner provides archComponentsOwner,
LocalSaveableStateRegistry provides savableStateRegistry,
content = content
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.LocalSystemTheme
import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
import androidx.compose.ui.draganddrop.WebDragAndDropManager
import androidx.compose.ui.events.EventTargetListener
import androidx.compose.ui.geometry.Offset
Expand Down Expand Up @@ -431,6 +432,7 @@ internal class ComposeWindow(
LocalLifecycleOwner provides this,
LocalInternalViewModelStoreOwner provides this,
LocalInternalNavigationEventDispatcherOwner provides this,
LocalCompatNavigationEventDispatcherOwner provides this,
LocalInteropContainer provides interopContainer,
LocalActiveClipEventsTarget provides {
(platformContext.textInputService as WebTextInputService).getBackingInput() ?: canvas
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,13 @@

package androidx.navigation.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
import androidx.compose.runtime.saveable.SaveableStateRegistry
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.ComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.StateRestorationTester
import androidx.compose.ui.test.runComposeUiTest
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner

@OptIn(ExperimentalTestApi::class)
internal fun runComposeUiTestOnUiThread(block: ComposeUiTest.() -> Unit) {
runComposeUiTest {
runOnUiThread { block() }
}
}

@OptIn(ExperimentalTestApi::class)
internal fun ComposeUiTest.setContentWithLifecycleOwner(content: @Composable () -> Unit) {
setContent {
CompositionLocalProvider(LocalLifecycleOwner provides TestLifecycleOwner(Lifecycle.State.RESUMED)) {
content()
}
}
}
Loading