diff --git a/components/gradle.properties b/components/gradle.properties index 1555ac6273..f4fd2d49f3 100644 --- a/components/gradle.properties +++ b/components/gradle.properties @@ -8,13 +8,13 @@ android.useAndroidX=true #Versions kotlin.version=1.9.21 -compose.version=1.6.0-dev1323 +compose.version=1.6.0-dev1334 agp.version=8.1.2 #Compose org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true org.jetbrains.compose.experimental.macos.enabled=true -org.jetbrains.compose.experimental.uikit.enabled=true compose.resources.always.generate.accessors=true compose.desktop.verbose=true compose.useMavenLocal=false diff --git a/components/gradle/libs.versions.toml b/components/gradle/libs.versions.toml new file mode 100644 index 0000000000..8eaf2cf157 --- /dev/null +++ b/components/gradle/libs.versions.toml @@ -0,0 +1,7 @@ +[versions] +kotlinx-coroutines = "1.8.0-RC" + +[libraries] +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } \ No newline at end of file diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts index 521e303e38..af97e41f33 100644 --- a/components/resources/demo/shared/build.gradle.kts +++ b/components/resources/demo/shared/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl plugins { kotlin("multiplatform") @@ -35,6 +36,11 @@ kotlin { } binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } listOf( macosX64(), diff --git a/components/resources/demo/shared/src/wasmJsMain/kotlin/main.wasm.kt b/components/resources/demo/shared/src/wasmJsMain/kotlin/main.wasm.kt new file mode 100644 index 0000000000..bcd51ccb26 --- /dev/null +++ b/components/resources/demo/shared/src/wasmJsMain/kotlin/main.wasm.kt @@ -0,0 +1,16 @@ +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.configureWebResources +import org.jetbrains.compose.resources.demo.shared.UseResources + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class) +fun main() { + configureWebResources { + // Not necessary - It's the same as the default. We add it here just to present this feature. + resourcePathMapping { path -> "./$path" } + } + CanvasBasedWindow("Resources demo + K/Wasm") { + UseResources() + } +} \ No newline at end of file diff --git a/components/resources/demo/shared/src/wasmJsMain/resources/index.html b/components/resources/demo/shared/src/wasmJsMain/resources/index.html new file mode 100644 index 0000000000..27b33ced67 --- /dev/null +++ b/components/resources/demo/shared/src/wasmJsMain/resources/index.html @@ -0,0 +1,11 @@ + + + + + Resources demo + K/Wasm + + + + + + \ No newline at end of file diff --git a/components/resources/library/build.gradle.kts b/components/resources/library/build.gradle.kts index 8fa273110e..ba8ef3cfb9 100644 --- a/components/resources/library/build.gradle.kts +++ b/components/resources/library/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl plugins { kotlin("multiplatform") @@ -31,6 +32,15 @@ kotlin { }) } } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser { + testTask(Action { + // TODO: fix the test setup and enable + enabled = false + }) + } + } macosX64() macosArm64() @@ -50,18 +60,18 @@ kotlin { // ┌───┴───┬──│────────┐ │ // │ native │ jvmAndAndroid // │ ┌───┴───┐ │ ┌───┴───┐ - // js ios macos desktop android + // web ios macos desktop android val commonMain by getting { dependencies { implementation(compose.runtime) implementation(compose.foundation) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation(libs.kotlinx.coroutines.core) } } val commonTest by getting { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + implementation(libs.kotlinx.coroutines.test) implementation(kotlin("test")) } } @@ -96,7 +106,7 @@ kotlin { dependencies { implementation(compose.desktop.currentOs) implementation("org.jetbrains.compose.ui:ui-test-junit4:$composeVersion") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.3") + implementation(libs.kotlinx.coroutines.swing) } } val androidMain by getting { @@ -122,12 +132,24 @@ kotlin { dependsOn(skikoTest) dependsOn(blockingTest) } - val jsMain by getting { + val webMain by creating { dependsOn(skikoMain) } - val jsTest by getting { + val jsMain by getting { + dependsOn(webMain) + } + val wasmJsMain by getting { + dependsOn(webMain) + } + val webTest by creating { dependsOn(skikoTest) } + val jsTest by getting { + dependsOn(webTest) + } + val wasmJsTest by getting { + dependsOn(webTest) + } } } @@ -169,3 +191,10 @@ configureMavenPublication( artifactId = "components-resources", name = "Resources for Compose JB" ) + +afterEvaluate { + // TODO(o.k.): remove this after we refactor jsAndWasmMain source set in skiko to get rid of broken "common" js-interop + tasks.configureEach { + if (name == "compileWebMainKotlinMetadata") enabled = false + } +} \ No newline at end of file diff --git a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ImageResources.wasmJs.kt b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ImageResources.wasmJs.kt new file mode 100644 index 0000000000..4de24550e7 --- /dev/null +++ b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ImageResources.wasmJs.kt @@ -0,0 +1,13 @@ +package org.jetbrains.compose.resources + +import org.jetbrains.compose.resources.vector.xmldom.Element +import org.jetbrains.compose.resources.vector.xmldom.ElementImpl +import org.jetbrains.compose.resources.vector.xmldom.MalformedXMLException +import org.w3c.dom.parsing.DOMParser + +internal actual fun ByteArray.toXmlElement(): Element { + val xmlString = decodeToString() + val xmlDom = DOMParser().parseFromString(xmlString, "application/xml".toJsString()) + val domElement = xmlDom.documentElement ?: throw MalformedXMLException("missing documentElement") + return ElementImpl(domElement.unsafeCast()) +} \ No newline at end of file diff --git a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt new file mode 100644 index 0000000000..72513cc983 --- /dev/null +++ b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt @@ -0,0 +1,51 @@ +package org.jetbrains.compose.resources + +import kotlinx.browser.window +import kotlinx.coroutines.await +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Int8Array +import org.w3c.fetch.Response +import kotlin.wasm.unsafe.UnsafeWasmMemoryApi +import kotlin.wasm.unsafe.withScopedMemoryAllocator + +/** + * Reads the content of the resource file at the specified path and returns it as a byte array. + * + * @param path The path of the file to read in the resource's directory. + * @return The content of the file as a byte array. + */ +@ExperimentalResourceApi +actual suspend fun readResourceBytes(path: String): ByteArray { + val resPath = WebResourcesConfiguration.getResourcePath(path) + val response = window.fetch(resPath).await() + if (!response.ok) { + throw MissingResourceException(resPath) + } + return response.arrayBuffer().await().toByteArray() +} + +private fun ArrayBuffer.toByteArray(): ByteArray { + val source = Int8Array(this, 0, byteLength) + return jsInt8ArrayToKotlinByteArray(source) +} + +@JsFun( + """ (src, size, dstAddr) => { + const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size); + mem8.set(src); + } +""" +) +internal external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int) + +internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray { + val size = x.length + + @OptIn(UnsafeWasmMemoryApi::class) + return withScopedMemoryAllocator { allocator -> + val memBuffer = allocator.allocate(size) + val dstAddress = memBuffer.address.toInt() + jsExportInt8ArrayToWasm(x, size, dstAddress) + ByteArray(size) { i -> (memBuffer + i).loadByte() } + } +} \ No newline at end of file diff --git a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt new file mode 100644 index 0000000000..8a9b566a18 --- /dev/null +++ b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt @@ -0,0 +1,19 @@ +package org.jetbrains.compose.resources.vector.xmldom + +import org.w3c.dom.Element as DomElement + +internal class ElementImpl(val element: DomElement): NodeImpl(element), Element { + override val textContent: String? + get() = element.textContent + + override val localName: String + get() = element.localName + + override val namespaceURI: String + get() = element.namespaceURI ?: "" + + override fun getAttributeNS(nameSpaceURI: String, localName: String): String = + element.getAttributeNS(nameSpaceURI, localName) ?: "" + + override fun getAttribute(name: String): String = element.getAttribute(name) ?: "" +} diff --git a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt new file mode 100644 index 0000000000..09a0f44200 --- /dev/null +++ b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt @@ -0,0 +1,31 @@ +package org.jetbrains.compose.resources.vector.xmldom + +import org.w3c.dom.Element as DomElement +import org.w3c.dom.Node as DomNode + +internal open class NodeImpl(val n: DomNode): Node { + override val textContent: String? + get() = n.textContent + + override val nodeName: String + get() = n.nodeName + + override val localName = "" /* localName is not a Node property, only applies to Elements and Attrs */ + + override val namespaceURI = "" /* namespaceURI is not a Node property, only applies to Elements and Attrs */ + + override val childNodes: NodeList by lazy { + object: NodeList { + override fun item(i: Int): Node { + val child = n.childNodes.item(i) + ?: throw IndexOutOfBoundsException("no child node accessible at index=$i") + return if (child is DomElement) ElementImpl(child) else NodeImpl(child) + } + + override val length: Int = n.childNodes.length + } + } + + override fun lookupPrefix(namespaceURI: String): String = n.lookupPrefix(namespaceURI) ?: "" + +} diff --git a/components/resources/library/src/wasmJsTest/kotlin/org/jetbrains/compose/resources/TestUtils.wasmJs.kt b/components/resources/library/src/wasmJsTest/kotlin/org/jetbrains/compose/resources/TestUtils.wasmJs.kt new file mode 100644 index 0000000000..52a1bcaab2 --- /dev/null +++ b/components/resources/library/src/wasmJsTest/kotlin/org/jetbrains/compose/resources/TestUtils.wasmJs.kt @@ -0,0 +1,7 @@ +package org.jetbrains.compose.resources + +import kotlinx.coroutines.CoroutineScope + +actual fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) { + TODO("To be implemented in PR 4031") +} \ No newline at end of file diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/PlatformState.js.kt b/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/PlatformState.web.kt similarity index 100% rename from components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/PlatformState.js.kt rename to components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/PlatformState.web.kt diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt b/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt similarity index 100% rename from components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt rename to components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt