Skip to content

Commit

Permalink
html interop wasm support (untested) (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
sargunv authored Dec 31, 2024
1 parent 1aba287 commit 417c306
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 49 deletions.
14 changes: 10 additions & 4 deletions lib/compose-html-interop/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
@file:OptIn(ExperimentalWasmDsl::class)

import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl

plugins {
id("library-conventions")
id(libs.plugins.kotlin.multiplatform.get().pluginId)
Expand All @@ -16,12 +20,14 @@ mavenPublishing {

kotlin {
js(IR) { browser() }
wasmJs { browser() }

sourceSets {
commonMain.dependencies {
implementation(kotlin("stdlib-js"))
implementation(compose.foundation)
}
commonMain.dependencies { implementation(compose.foundation) }

jsMain.dependencies { implementation(kotlin("stdlib-js")) }

wasmJsMain.dependencies { implementation(libs.kotlinx.browser) }

commonTest.dependencies {
implementation(kotlin("test"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import kotlinx.browser.document
import org.w3c.dom.HTMLElement

@Composable
public fun <T : HTMLElement> HtmlElement(
Expand All @@ -15,20 +13,9 @@ public fun <T : HTMLElement> HtmlElement(
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current

val container =
rememberDomNode(parent = document.body!!) {
document.createElement("div").unsafeCast<HTMLElement>().apply {
style.position = "absolute"
style.margin = "0px"
}
}

val container = rememberContainerNode()
val child = rememberDomNode(parent = container, factory = factory)

SnapshotEffect(child) { update(it) }

Box(modifier.onGloballyPositioned { container.matchLayout(it, density) })

HtmlFocusAdapter(container)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalFocusManager
import org.w3c.dom.HTMLElement

@Composable
internal fun HtmlFocusAdapter(container: HTMLElement) {
Expand All @@ -29,10 +28,10 @@ internal fun HtmlFocusAdapter(container: HTMLElement) {
modifier =
Modifier.focusRequester(head).onFocusChanged {
if (it.isFocused && !ownFocusRequest) {
val htmlHead = currentContainer.firstElementChild
val htmlHead = currentContainer.headChild
if (htmlHead != null) {
focusManager.clearFocus(force = true)
htmlHead.unsafeCast<HTMLElement>().focus()
htmlHead.focus()
} else {
ownFocusRequest = true
tail.requestFocus()
Expand All @@ -47,10 +46,10 @@ internal fun HtmlFocusAdapter(container: HTMLElement) {
modifier =
Modifier.focusRequester(tail).onFocusChanged {
if (it.isFocused && !ownFocusRequest) {
val htmlTail = currentContainer.lastElementChild
val htmlTail = currentContainer.tailChild
if (htmlTail != null) {
focusManager.clearFocus(force = true)
htmlTail.unsafeCast<HTMLElement>().focus()
htmlTail.focus()
} else {
ownFocusRequest = true
head.requestFocus()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,23 @@
package dev.sargunv.composehtmlinterop

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import org.w3c.dom.HTMLElement
import org.w3c.dom.Node

public expect abstract class HTMLElement {
public fun focus()
}

internal fun Dp.toCssValue(): String = "${value}px"

internal fun HTMLElement.matchLayout(layoutCoordinates: LayoutCoordinates, density: Density) {
with(density) {
style.apply {
val rect = layoutCoordinates.boundsInWindow()
width = rect.width.toDp().toCssValue()
height = rect.height.toDp().toCssValue()
left = rect.left.toDp().toCssValue()
top = rect.top.toDp().toCssValue()
}
}
}
@Composable internal expect fun rememberContainerNode(): HTMLElement

internal expect fun HTMLElement.matchLayout(layoutCoordinates: LayoutCoordinates, density: Density)

@Composable
internal fun <T : Node> rememberDomNode(parent: Node, factory: () -> T): T {
return remember(key1 = parent, calculation = factory).also { child ->
DisposableEffect(parent, child) {
parent.insertBefore(child, parent.firstChild)
onDispose { parent.removeChild(child) }
}
}
}
internal expect fun <T : HTMLElement> rememberDomNode(parent: HTMLElement, factory: () -> T): T

internal expect val HTMLElement.headChild: HTMLElement?

internal expect val HTMLElement.tailChild: HTMLElement?
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dev.sargunv.composehtmlinterop

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.unit.Density
import kotlinx.browser.document

public actual typealias HTMLElement = org.w3c.dom.HTMLElement

@Composable
internal actual fun rememberContainerNode(): HTMLElement =
rememberDomNode(parent = document.body!!) {
document.createElement("div").unsafeCast<HTMLElement>().apply {
style.position = "absolute"
style.margin = "0px"
}
}

internal actual fun HTMLElement.matchLayout(
layoutCoordinates: LayoutCoordinates,
density: Density,
) {
with(density) {
style.apply {
val rect = layoutCoordinates.boundsInWindow()
width = rect.width.toDp().toCssValue()
height = rect.height.toDp().toCssValue()
left = rect.left.toDp().toCssValue()
top = rect.top.toDp().toCssValue()
}
}
}

@Composable
internal actual fun <T : HTMLElement> rememberDomNode(parent: HTMLElement, factory: () -> T): T {
return remember(key1 = parent, calculation = factory).also { child ->
DisposableEffect(parent, child) {
parent.insertBefore(child, parent.firstChild)
onDispose { parent.removeChild(child) }
}
}
}

internal actual val HTMLElement.headChild
get() = firstElementChild?.unsafeCast<HTMLElement>()

internal actual val HTMLElement.tailChild
get() = lastElementChild?.unsafeCast<HTMLElement>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dev.sargunv.composehtmlinterop

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.unit.Density
import kotlinx.browser.document

public actual typealias HTMLElement = org.w3c.dom.HTMLElement

@Composable
internal actual fun rememberContainerNode() =
rememberDomNode(parent = document.body!!) {
document.createElement("div").unsafeCast<HTMLElement>().apply {
style.position = "absolute"
style.margin = "0px"
}
}

internal actual fun HTMLElement.matchLayout(
layoutCoordinates: LayoutCoordinates,
density: Density,
) {
with(density) {
style.apply {
val rect = layoutCoordinates.boundsInWindow()
width = rect.width.toDp().toCssValue()
height = rect.height.toDp().toCssValue()
left = rect.left.toDp().toCssValue()
top = rect.top.toDp().toCssValue()
}
}
}

@Composable
internal actual fun <T : HTMLElement> rememberDomNode(parent: HTMLElement, factory: () -> T): T {
return remember(key1 = parent, calculation = factory).also { child ->
DisposableEffect(parent, child) {
parent.insertBefore(child, parent.firstChild)
onDispose { parent.removeChild(child) }
}
}
}

internal actual val HTMLElement.headChild
get() = firstElementChild?.unsafeCast<HTMLElement>()

internal actual val HTMLElement.tailChild
get() = lastElementChild?.unsafeCast<HTMLElement>()
2 changes: 1 addition & 1 deletion lib/kotlin-maplibre-js/MODULE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Module maplibre-gl-js-kotlin
# Module kotlin-maplibre-js

Kotlin bindings for [MapLibre GL JS](https://www.npmjs.com/package/maplibre-gl).
2 changes: 1 addition & 1 deletion lib/kotlin-maplibre-js/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
mavenPublishing {
pom {
name = "MapLibre GL JS Kotlin"
description = "Kotlin wrapper for MapLibre GL JS."
description = "Kotlin bindings for MapLibre GL JS."
url = "https://github.com/sargunv/maplibre-compose"
}
}
Expand Down

0 comments on commit 417c306

Please sign in to comment.