-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6b42980
commit 881abc6
Showing
8 changed files
with
442 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
...on/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/BrowserViewportWindow.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
// From Slack by OliverO | ||
// See: https://kotlinlang.slack.com/archives/C01F2HV7868/p1660083429206369?thread_ts=1660083398.571449&cid=C01F2HV7868 | ||
// adapted with scaling fix from https://github.com/OliverO2/compose-counting-grid/blob/master/src/frontendJsMain/kotlin/BrowserViewportWindow.kt | ||
|
||
@file:Suppress( | ||
"INVISIBLE_MEMBER", | ||
"INVISIBLE_REFERENCE", | ||
"EXPOSED_PARAMETER_TYPE" | ||
) // WORKAROUND: ComposeWindow and ComposeLayer are internal | ||
|
||
package com.bumble.appyx.navigation.integration | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.window.ComposeWindow | ||
import kotlinx.browser.document | ||
import kotlinx.browser.window | ||
import org.w3c.dom.HTMLCanvasElement | ||
import org.w3c.dom.HTMLStyleElement | ||
import org.w3c.dom.HTMLTitleElement | ||
|
||
private const val CANVAS_ELEMENT_ID = "ComposeTarget" // Hardwired into ComposeWindow | ||
|
||
/** | ||
* A Skiko/Canvas-based top-level window using the browser's entire viewport. Supports resizing. | ||
*/ | ||
@Suppress("FunctionNaming") | ||
fun BrowserViewportWindow( | ||
title: String = "Untitled", | ||
content: @Composable ComposeWindow.() -> Unit | ||
) { | ||
val htmlHeadElement = document.head!! | ||
htmlHeadElement.appendChild( | ||
(document.createElement("style") as HTMLStyleElement).apply { | ||
type = "text/css" | ||
appendChild( | ||
document.createTextNode( | ||
""" | ||
html, body { | ||
overflow: hidden; | ||
margin: 0 !important; | ||
padding: 0 !important; | ||
} | ||
#$CANVAS_ELEMENT_ID { | ||
outline: none; | ||
} | ||
""".trimIndent() | ||
) | ||
) | ||
} | ||
) | ||
|
||
fun HTMLCanvasElement.fillViewportSize() { | ||
setAttribute("width", "${window.innerWidth}") | ||
setAttribute("height", "${window.innerHeight}") | ||
} | ||
|
||
val canvas = (document.getElementById(CANVAS_ELEMENT_ID) as HTMLCanvasElement).apply { | ||
fillViewportSize() | ||
} | ||
|
||
ComposeWindow().apply { | ||
window.addEventListener("resize", { | ||
canvas.fillViewportSize() | ||
layer.layer.attachTo(canvas) | ||
layer.layer.needRedraw() | ||
val scale = layer.layer.contentScale | ||
layer.setSize( | ||
(canvas.width / scale * density.density).toInt(), | ||
(canvas.height / scale * density.density).toInt() | ||
) | ||
}) | ||
|
||
// WORKAROUND: ComposeWindow does not implement `setTitle(title)` | ||
val htmlTitleElement = ( | ||
htmlHeadElement.getElementsByTagName("title").item(0) | ||
?: document.createElement("title").also { htmlHeadElement.appendChild(it) } | ||
) as HTMLTitleElement | ||
htmlTitleElement.textContent = title | ||
|
||
setContent { | ||
content(this) | ||
} | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...mon/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/MainIntegrationPoint.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.bumble.appyx.navigation.integration | ||
|
||
import com.bumble.appyx.navigation.integrationpoint.IntegrationPoint | ||
|
||
class MainIntegrationPoint : IntegrationPoint() { | ||
override val isChangingConfigurations: Boolean | ||
get() = false | ||
|
||
@Suppress("EmptyFunctionBlock") | ||
override fun onRootFinished() { | ||
} | ||
|
||
@Suppress("EmptyFunctionBlock") | ||
override fun handleUpNavigation() { | ||
|
||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
...ation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/WebNodeHost.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package com.bumble.appyx.navigation.integration | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.CompositionLocalProvider | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.rememberCoroutineScope | ||
import androidx.compose.ui.Modifier | ||
import com.bumble.appyx.navigation.integrationpoint.IntegrationPoint | ||
import com.bumble.appyx.navigation.node.Node | ||
import com.bumble.appyx.navigation.platform.LocalOnBackPressedDispatcherOwner | ||
import com.bumble.appyx.navigation.platform.OnBackPressedDispatcher | ||
import com.bumble.appyx.navigation.platform.OnBackPressedDispatcherOwner | ||
import com.bumble.appyx.navigation.platform.PlatformLifecycleRegistry | ||
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectory | ||
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.launch | ||
|
||
/** | ||
* Composable function to host [Node]. | ||
* | ||
* This convenience wrapper provides an [OnBackPressedDispatcherOwner] hooked up to the | ||
* [.onBackPressedEvents] flow to simplify implementing the global "go back" functionality | ||
* that is a common concept in the Appyx framework. | ||
*/ | ||
@Suppress("ComposableParamOrder") | ||
@Composable | ||
fun <N : Node> WebNodeHost( | ||
screenSize: ScreenSize, | ||
onBackPressedEvents: Flow<Unit>, | ||
modifier: Modifier = Modifier, | ||
integrationPoint: IntegrationPoint = remember { MainIntegrationPoint() }, | ||
customisations: NodeCustomisationDirectory = remember { NodeCustomisationDirectoryImpl() }, | ||
factory: NodeFactory<N> | ||
) { | ||
val platformLifecycleRegistry = remember { | ||
PlatformLifecycleRegistry() | ||
} | ||
val onBackPressedDispatcherOwner = remember<OnBackPressedDispatcherOwner> { | ||
object : OnBackPressedDispatcherOwner { | ||
override val onBackPressedDispatcher: OnBackPressedDispatcher = | ||
OnBackPressedDispatcher { integrationPoint.handleUpNavigation() } | ||
} | ||
} | ||
|
||
val scope = rememberCoroutineScope() | ||
LaunchedEffect(onBackPressedEvents) { | ||
scope.launch { | ||
onBackPressedEvents.collect { | ||
onBackPressedDispatcherOwner.onBackPressedDispatcher.onBackPressed() | ||
} | ||
} | ||
} | ||
|
||
CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides onBackPressedDispatcherOwner) { | ||
NodeHost( | ||
lifecycle = platformLifecycleRegistry, | ||
integrationPoint = integrationPoint, | ||
modifier = modifier, | ||
customisations = customisations, | ||
screenSize = screenSize, | ||
factory = factory, | ||
) | ||
} | ||
} |
59 changes: 59 additions & 0 deletions
59
...ommon/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedCallback.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package com.bumble.appyx.navigation.platform | ||
|
||
interface Cancellable { | ||
/** | ||
* Cancel the subscription. This call should be idempotent, making it safe to | ||
* call multiple times. | ||
*/ | ||
fun cancel() | ||
} | ||
|
||
/** | ||
* Create a [OnBackPressedCallback]. | ||
* | ||
* @param isEnabled The default enabled state for this callback. | ||
* @see .setEnabled | ||
*/ | ||
abstract class OnBackPressedCallback( | ||
/** | ||
* Set the enabled state of the callback. Only when this callback | ||
* is enabled will it receive callbacks to [.handleOnBackPressed]. | ||
* | ||
* @param isEnabled whether the callback should be considered enabled | ||
*/ | ||
var isEnabled: Boolean | ||
) { | ||
/** | ||
* Checks whether this callback should be considered enabled. Only when this callback | ||
* is enabled will it receive callbacks to [.handleOnBackPressed]. | ||
* | ||
* @return Whether this callback should be considered enabled. | ||
*/ | ||
private val cancellables: MutableList<Cancellable> = mutableListOf() | ||
|
||
/** | ||
* Removes this callback from any [OnBackPressedDispatcher] it is currently | ||
* added to. | ||
*/ | ||
fun remove() { | ||
for (cancellable in cancellables) { | ||
cancellable.cancel() | ||
} | ||
} | ||
|
||
/** | ||
* Callback for handling the [OnBackPressedDispatcher.onBackPressed] event. | ||
*/ | ||
abstract fun handleOnBackPressed() | ||
fun addCancellable(cancellable: Cancellable) { | ||
run { | ||
cancellables.add(cancellable) | ||
} | ||
} | ||
|
||
fun removeCancellable(cancellable: Cancellable) { | ||
run { | ||
cancellables.remove(cancellable) | ||
} | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
...mon/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedDispatcher.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package com.bumble.appyx.navigation.platform | ||
|
||
/** | ||
* Adapted from Android's OnBackPressedDispatcher. | ||
* | ||
* Create a new OnBackPressedDispatcher that dispatches [.onBackPressed] events | ||
* to one or more [OnBackPressedCallback] instances. | ||
* | ||
* @param fallbackOnBackPressed The Runnable that should be triggered if | ||
* [.onBackPressed] is called when no [OnBackPressedCallback] have been registered. | ||
*/ | ||
class OnBackPressedDispatcher(private val fallbackOnBackPressed: (() -> Unit)? = null) { | ||
val onBackPressedCallbacks: ArrayDeque<OnBackPressedCallback> = | ||
ArrayDeque() | ||
|
||
/** | ||
* Internal implementation of [.addCallback] that gives | ||
* access to the [Cancellable] that specifically removes this callback from | ||
* the dispatcher without relying on [OnBackPressedCallback.remove] which | ||
* is what external developers should be using. | ||
* | ||
* @param onBackPressedCallback The callback to add | ||
* @return a [Cancellable] which can be used to [cancel][Cancellable.cancel] | ||
* the callback and remove it from the set of OnBackPressedCallbacks. | ||
*/ | ||
fun addCancellableCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable { | ||
onBackPressedCallbacks.add(onBackPressedCallback) | ||
val cancellable = OnBackPressedCancellable(onBackPressedCallback) | ||
onBackPressedCallback.addCancellable(cancellable) | ||
return cancellable | ||
} | ||
|
||
/** | ||
* Trigger a call to the currently added [callbacks][OnBackPressedCallback] in reverse | ||
* order in which they were added. Only if the most recently added callback is not | ||
* [enabled][OnBackPressedCallback.isEnabled] | ||
* will any previously added callback be called. | ||
* | ||
* | ||
* If [.hasEnabledCallbacks] is `false` when this method is called, the | ||
* fallback Runnable set by [the constructor][.OnBackPressedDispatcher] | ||
* will be triggered. | ||
*/ | ||
fun onBackPressed() { | ||
onBackPressedCallbacks.reversed().forEach { callback -> | ||
if (callback.isEnabled) { | ||
callback.handleOnBackPressed() | ||
return | ||
} | ||
} | ||
fallbackOnBackPressed?.invoke() | ||
} | ||
|
||
private inner class OnBackPressedCancellable(onBackPressedCallback: OnBackPressedCallback) : | ||
Cancellable { | ||
private val onBackPressedCallback: OnBackPressedCallback | ||
|
||
init { | ||
this.onBackPressedCallback = onBackPressedCallback | ||
} | ||
|
||
override fun cancel() { | ||
onBackPressedCallbacks.remove(onBackPressedCallback) | ||
onBackPressedCallback.removeCancellable(this) | ||
} | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
.../common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformBackHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package com.bumble.appyx.navigation.platform | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.DisposableEffect | ||
import androidx.compose.runtime.ProvidableCompositionLocal | ||
import androidx.compose.runtime.SideEffect | ||
import androidx.compose.runtime.compositionLocalOf | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.rememberUpdatedState | ||
|
||
@Suppress("CompositionLocalAllowlist") | ||
val LocalOnBackPressedDispatcherOwner: ProvidableCompositionLocal<OnBackPressedDispatcherOwner> = | ||
compositionLocalOf { | ||
object : OnBackPressedDispatcherOwner { | ||
override val onBackPressedDispatcher: OnBackPressedDispatcher | ||
get() = OnBackPressedDispatcher(null) | ||
} | ||
} | ||
|
||
interface OnBackPressedDispatcherOwner { | ||
val onBackPressedDispatcher: OnBackPressedDispatcher | ||
} | ||
|
||
@Composable | ||
actual fun PlatformBackHandler( | ||
enabled: Boolean, | ||
onBack: () -> Unit | ||
) { | ||
// Safely update the current `onBack` lambda when a new one is provided | ||
val currentOnBack by rememberUpdatedState(onBack) | ||
// Remember in Composition a back callback that calls the `onBack` lambda | ||
val backCallback = remember<OnBackPressedCallback> { | ||
object : OnBackPressedCallback(enabled) { | ||
override fun handleOnBackPressed() { | ||
currentOnBack() | ||
} | ||
} | ||
} | ||
// On every successful composition, update the callback with the `enabled` value | ||
SideEffect { | ||
backCallback.isEnabled = enabled | ||
} | ||
|
||
// register for back events only whilst present in the composition | ||
val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) { | ||
"No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner" | ||
}.onBackPressedDispatcher | ||
DisposableEffect(backDispatcher) { | ||
val cancellable = backDispatcher.addCancellableCallback(backCallback) | ||
|
||
onDispose { | ||
cancellable.cancel() | ||
} | ||
} | ||
} |
Oops, something went wrong.