Skip to content

Commit

Permalink
Add missing wasm source tree
Browse files Browse the repository at this point in the history
  • Loading branch information
mmartosdev committed Jan 11, 2024
1 parent 6b42980 commit 881abc6
Show file tree
Hide file tree
Showing 8 changed files with 442 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.bumble.appyx.navigation.platform

import androidx.compose.ui.util.fastForEachReversed

/**
* Adapted from Android's OnBackPressedDispatcher.
*
Expand Down Expand Up @@ -44,7 +42,7 @@ class OnBackPressedDispatcher(private val fallbackOnBackPressed: (() -> Unit)? =
* will be triggered.
*/
fun onBackPressed() {
onBackPressedCallbacks.fastForEachReversed { callback ->
onBackPressedCallbacks.reversed().forEach { callback ->
if (callback.isEnabled) {
callback.handleOnBackPressed()
return
Expand Down
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)
}
}
}
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() {

}
}
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,
)
}
}
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)
}
}
}
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)
}
}
}
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()
}
}
}
Loading

0 comments on commit 881abc6

Please sign in to comment.