Browse Source

Merge branch 'master' into samples_against_master

samples_against_master
Shagen Ogandzhanian 3 years ago
parent
commit
ddd0fe0645
  1. 80
      tutorials/Image_And_Icons_Manipulations/README.md
  2. BIN
      tutorials/Image_And_Icons_Manipulations/draw_image_into_canvas.png
  3. 8
      web/core/build.gradle.kts
  4. 2
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt
  5. 7
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt
  6. 4
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt
  7. 200
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/transform.kt
  8. 108
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt
  9. 6
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt
  10. 1
      web/core/src/jsTest/kotlin/CSSStylesheetTests.kt
  11. 1
      web/core/src/jsTest/kotlin/CSSUnitApiTests.kt
  12. 1
      web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt
  13. 1
      web/core/src/jsTest/kotlin/DomSideEffectTests.kt
  14. 2
      web/core/src/jsTest/kotlin/FailingTestCases.kt
  15. 1
      web/core/src/jsTest/kotlin/InlineStyleTests.kt
  16. 13
      web/core/src/jsTest/kotlin/StaticComposableTests.kt
  17. 4
      web/core/src/jsTest/kotlin/css/CSSBackgroundTests.kt
  18. 2
      web/core/src/jsTest/kotlin/css/CSSBorderTests.kt
  19. 4
      web/core/src/jsTest/kotlin/css/CSSBoxTests.kt
  20. 4
      web/core/src/jsTest/kotlin/css/CSSDisplayTests.kt
  21. 2
      web/core/src/jsTest/kotlin/css/CSSFlexTests.kt
  22. 2
      web/core/src/jsTest/kotlin/css/CSSListStyleTests.kt
  23. 4
      web/core/src/jsTest/kotlin/css/CSSMarginTests.kt
  24. 4
      web/core/src/jsTest/kotlin/css/CSSOverflowTests.kt
  25. 4
      web/core/src/jsTest/kotlin/css/CSSPaddingTests.kt
  26. 5
      web/core/src/jsTest/kotlin/css/CSSTextTests.kt
  27. 4
      web/core/src/jsTest/kotlin/css/CSSUiTests.kt
  28. 2
      web/core/src/jsTest/kotlin/css/ColorTests.kt
  29. 2
      web/core/src/jsTest/kotlin/css/FilterTests.kt
  30. 4
      web/core/src/jsTest/kotlin/css/GridTests.kt
  31. 2
      web/core/src/jsTest/kotlin/css/TransformTests.kt
  32. 55
      web/core/src/jsTest/kotlin/elements/AttributesTests.kt
  33. 30
      web/core/src/jsTest/kotlin/elements/ElementsTests.kt
  34. 1
      web/core/src/jsTest/kotlin/elements/EventTests.kt
  35. 3
      web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt
  36. 1
      web/core/src/jsTest/kotlin/elements/TableTests.kt
  37. 4
      web/gradle.properties
  38. 33
      web/internal-web-core-runtime/build.gradle.kts
  39. 4
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/ComposeWebInternalApi.kt
  40. 73
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/DomApplier.kt
  41. 7
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/GlobalSnapshotManager.kt
  42. 5
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/JsMicrotasksDispatcher.kt
  43. 2
      web/settings.gradle.kts
  44. 42
      web/test-utils/build.gradle.kts
  45. 4
      web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/ComposeWebExperimentalTestsApi.kt
  46. 70
      web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt
  47. 82
      web/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt

80
tutorials/Image_And_Icons_Manipulations/README.md

@ -19,7 +19,7 @@ import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication {
Image(
painter = painterResource("sample.png"), // ImageBitmap
painter = painterResource("sample.png"),
contentDescription = "Sample",
modifier = Modifier.fillMaxSize()
)
@ -135,70 +135,66 @@ fun loadXmlImageVector(file: File, density: Density): ImageVector =
[XML vector drawable](../../artwork/compose-logo.xml)
## Drawing raw image data using native canvas
You may want to draw raw image data, in which case you can use `Canvas` and` drawIntoCanvas`.
## Drawing images using Canvas
```kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.graphics.withSave
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.loadSvgPainter
import androidx.compose.ui.res.loadXmlImageVector
import androidx.compose.ui.res.useResource
import androidx.compose.ui.window.singleWindowApplication
import org.jetbrains.skija.Bitmap
import org.jetbrains.skija.ColorAlphaType
import org.jetbrains.skija.ImageInfo
private val sample = useResource("sample.png", ::loadImageBitmap)
import org.xml.sax.InputSource
fun main() = singleWindowApplication {
val bitmap = remember { bitmapFromByteArray(sample.getBytes(), sample.width, sample.height) }
val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML)
val sample = remember {
useResource("sample.png", ::loadImageBitmap)
}
val ideaLogo = remember {
useResource("idea-logo.svg") { loadSvgPainter(it, density) }
}
val composeLogo = rememberVectorPainter(
remember {
useResource("compose-logo.xml") { loadXmlImageVector(InputSource(it), density) }
}
)
Canvas(
modifier = Modifier.fillMaxSize()
) {
drawIntoCanvas { canvas ->
canvas.drawImage(bitmap, Offset.Zero, Paint())
canvas.withSave {
canvas.drawImage(sample, Offset.Zero, Paint())
canvas.translate(sample.width.toFloat(), 0f)
with(ideaLogo) {
draw(ideaLogo.intrinsicSize)
}
canvas.translate(ideaLogo.intrinsicSize.width, 0f)
with(composeLogo) {
draw(Size(100f, 100f))
}
}
}
}
}
```
fun bitmapFromByteArray(pixels: ByteArray, width: Int, height: Int): ImageBitmap {
val bitmap = Bitmap()
bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.PREMUL))
bitmap.installPixels(bitmap.imageInfo, pixels, (width * 4).toLong())
return bitmap.asImageBitmap()
}
// creating byte array from BufferedImage
private fun ImageBitmap.getBytes(): ByteArray {
val buffer = IntArray(width * height)
readPixels(buffer)
val pixels = ByteArray(width * height * 4)
var index = 0
for (y in 0 until height) {
for (x in 0 until width) {
val pixel = buffer[y * width + x]
pixels[index++] = ((pixel and 0xFF)).toByte() // Blue component
pixels[index++] = (((pixel shr 8) and 0xFF)).toByte() // Green component
pixels[index++] = (((pixel shr 16) and 0xFF)).toByte() // Red component
pixels[index++] = (((pixel shr 24) and 0xFF)).toByte() // Alpha component
}
}
[PNG](sample.png)
return pixels
}
```
[SVG](../../artwork/idea-logo.svg)
<img alt="Drawing raw images" src="draw_image_into_canvas.png" height="496" />
[XML vector drawable](../../artwork/compose-logo.xml)
## Setting the application window icon

BIN
tutorials/Image_And_Icons_Manipulations/draw_image_into_canvas.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

8
web/core/build.gradle.kts

@ -30,12 +30,14 @@ kotlin {
val jsMain by getting {
dependencies {
implementation(project(":internal-web-core-runtime"))
implementation(kotlin("stdlib-js"))
}
}
val jsTest by getting {
dependencies {
implementation(project(":test-utils"))
implementation(kotlin("test-js"))
}
}
@ -45,5 +47,11 @@ kotlin {
implementation(compose.desktop.currentOs)
}
}
all {
languageSettings {
useExperimentalAnnotation("org.jetbrains.compose.web.testutils.ComposeWebExperimentalTestsApi")
}
}
}
}

2
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt

@ -11,6 +11,7 @@ import kotlinx.browser.document
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.internal.runtime.*
import org.w3c.dom.Element
import org.w3c.dom.HTMLBodyElement
import org.w3c.dom.get
@ -22,6 +23,7 @@ import org.w3c.dom.get
*
* @return the instance of the [Composition]
*/
@OptIn(ComposeWebInternalApi::class)
fun <TElement : Element> renderComposable(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,

7
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt

@ -9,16 +9,21 @@ import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.CHAN
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.INPUT
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.SELECT
import org.jetbrains.compose.web.events.*
import org.jetbrains.compose.web.internal.runtime.ComposeWebInternalApi
import org.jetbrains.compose.web.internal.runtime.NamedEventListener
import org.w3c.dom.DragEvent
import org.w3c.dom.TouchEvent
import org.w3c.dom.clipboard.ClipboardEvent
import org.w3c.dom.events.*
@OptIn(ComposeWebInternalApi::class)
open class SyntheticEventListener<T : SyntheticEvent<*>> internal constructor(
val event: String,
val options: Options,
val listener: (T) -> Unit
) : EventListener {
) : EventListener, NamedEventListener {
override val name: String = event
@Suppress("UNCHECKED_CAST")
override fun handleEvent(event: Event) {

4
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt

@ -7,6 +7,9 @@
package org.jetbrains.compose.web.css
import org.jetbrains.compose.web.internal.runtime.DomElementWrapper
import org.jetbrains.compose.web.internal.runtime.ComposeWebInternalApi
import org.w3c.dom.css.CSSStyleDeclaration
import kotlin.properties.ReadOnlyProperty
/**
@ -128,6 +131,7 @@ interface StyleHolder {
val variables: StylePropertyList
}
@OptIn(ComposeWebInternalApi::class)
@Suppress("EqualsOrHashCode")
open class StyleBuilderImpl : StyleBuilder, StyleHolder {
override val properties: MutableStylePropertyList = mutableListOf()

200
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/transform.kt

@ -18,39 +18,39 @@ interface TransformBuilder {
a2: Number, b2: Number, c2: Number, d2: Number,
a3: Number, b3: Number, c3: Number, d3: Number,
a4: Number, b4: Number, c4: Number, d4: Number
): Boolean
fun perspective(d: CSSLengthValue): Boolean
fun rotate(a: CSSAngleValue): Boolean
fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue): Boolean
fun rotateX(a: CSSAngleValue): Boolean
fun rotateY(a: CSSAngleValue): Boolean
fun rotateZ(a: CSSAngleValue): Boolean
fun scale(sx: Number): Boolean
fun scale(sx: Number, sy: Number): Boolean
fun scale3d(sx: Number, sy: Number, sz: Number): Boolean
fun scaleX(s: Number): Boolean
fun scaleY(s: Number): Boolean
fun scaleZ(s: Number): Boolean
fun skew(ax: CSSAngleValue): Boolean
fun skew(ax: CSSAngleValue, ay: CSSAngleValue): Boolean
fun skewX(a: CSSAngleValue): Boolean
fun skewY(a: CSSAngleValue): Boolean
fun translate(tx: CSSLengthValue): Boolean
fun translate(tx: CSSPercentageValue): Boolean
fun translate(tx: CSSLengthValue, ty: CSSLengthValue): Boolean
fun translate(tx: CSSLengthValue, ty: CSSPercentageValue): Boolean
fun translate(tx: CSSPercentageValue, ty: CSSLengthValue): Boolean
fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue): Boolean
fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue): Boolean
fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue): Boolean
fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue): Boolean
fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue): Boolean
fun translateX(tx: CSSLengthValue): Boolean
fun translateX(tx: CSSPercentageValue): Boolean
fun translateY(ty: CSSLengthValue): Boolean
fun translateY(ty: CSSPercentageValue): Boolean
fun translateZ(tz: CSSLengthValue): Boolean
)
fun perspective(d: CSSLengthValue)
fun rotate(a: CSSAngleValue)
fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue)
fun rotateX(a: CSSAngleValue)
fun rotateY(a: CSSAngleValue)
fun rotateZ(a: CSSAngleValue)
fun scale(sx: Number)
fun scale(sx: Number, sy: Number)
fun scale3d(sx: Number, sy: Number, sz: Number)
fun scaleX(s: Number)
fun scaleY(s: Number)
fun scaleZ(s: Number)
fun skew(ax: CSSAngleValue)
fun skew(ax: CSSAngleValue, ay: CSSAngleValue)
fun skewX(a: CSSAngleValue)
fun skewY(a: CSSAngleValue)
fun translate(tx: CSSLengthValue)
fun translate(tx: CSSPercentageValue)
fun translate(tx: CSSLengthValue, ty: CSSLengthValue)
fun translate(tx: CSSLengthValue, ty: CSSPercentageValue)
fun translate(tx: CSSPercentageValue, ty: CSSLengthValue)
fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue)
fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue)
fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue)
fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue)
fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue)
fun translateX(tx: CSSLengthValue)
fun translateX(tx: CSSPercentageValue)
fun translateY(ty: CSSLengthValue)
fun translateY(ty: CSSPercentageValue)
fun translateZ(tz: CSSLengthValue)
}
private class TransformBuilderImplementation : TransformBuilder {
@ -64,67 +64,133 @@ private class TransformBuilderImplementation : TransformBuilder {
a2: Number, b2: Number, c2: Number, d2: Number,
a3: Number, b3: Number, c3: Number, d3: Number,
a4: Number, b4: Number, c4: Number, d4: Number
) =
) {
transformations.add { "matrix3d($a1, $b1, $c1, $d1, $a2, $b2, $c2, $d2, $a3, $b3, $c3, $d3, $a4, $b4, $c4, $d4)" }
}
override fun perspective(d: CSSLengthValue) {
transformations.add { "perspective($d)" }
}
override fun perspective(d: CSSLengthValue) = transformations.add { "perspective($d)" }
override fun rotate(a: CSSAngleValue) {
transformations.add { "rotate($a)" }
}
override fun rotate(a: CSSAngleValue) = transformations.add { "rotate($a)" }
override fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue) =
override fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue) {
transformations.add({ "rotate3d($x, $y, $z, $a)" })
}
override fun rotateX(a: CSSAngleValue) {
transformations.add { "rotateX($a)" }
}
override fun rotateY(a: CSSAngleValue) {
transformations.add { "rotateY($a)" }
}
override fun rotateX(a: CSSAngleValue) = transformations.add { "rotateX($a)" }
override fun rotateY(a: CSSAngleValue) = transformations.add { "rotateY($a)" }
override fun rotateZ(a: CSSAngleValue) = transformations.add { "rotateZ($a)" }
override fun rotateZ(a: CSSAngleValue) {
transformations.add { "rotateZ($a)" }
}
override fun scale(sx: Number) {
transformations.add { "scale($sx)" }
}
override fun scale(sx: Number) = transformations.add { "scale($sx)" }
override fun scale(sx: Number, sy: Number) = transformations.add { "scale($sx, $sy)" }
override fun scale3d(sx: Number, sy: Number, sz: Number) =
override fun scale(sx: Number, sy: Number) {
transformations.add { "scale($sx, $sy)" }
}
override fun scale3d(sx: Number, sy: Number, sz: Number) {
transformations.add { "scale3d($sx, $sy, $sz)" }
}
override fun scaleX(s: Number) = transformations.add { "scaleX($s)" }
override fun scaleY(s: Number) = transformations.add { "scaleY($s)" }
override fun scaleZ(s: Number) = transformations.add { "scaleZ($s)" }
override fun scaleX(s: Number) {
transformations.add { "scaleX($s)" }
}
override fun skew(ax: CSSAngleValue) = transformations.add { "skew($ax)" }
override fun skew(ax: CSSAngleValue, ay: CSSAngleValue) = transformations.add { "skew($ax, $ay)" }
override fun skewX(a: CSSAngleValue) = transformations.add { "skewX($a)" }
override fun skewY(a: CSSAngleValue) = transformations.add { "skewY($a)" }
override fun scaleY(s: Number) {
transformations.add { "scaleY($s)" }
}
override fun translate(tx: CSSLengthValue) = transformations.add { "translate($tx)" }
override fun translate(tx: CSSPercentageValue) = transformations.add { "translate($tx)" }
override fun scaleZ(s: Number) {
transformations.add { "scaleZ($s)" }
}
override fun translate(tx: CSSLengthValue, ty: CSSLengthValue) =
override fun skew(ax: CSSAngleValue) {
transformations.add { "skew($ax)" }
}
override fun skew(ax: CSSAngleValue, ay: CSSAngleValue) {
transformations.add { "skew($ax, $ay)" }
}
override fun skewX(a: CSSAngleValue) {
transformations.add { "skewX($a)" }
}
override fun skewY(a: CSSAngleValue) {
transformations.add { "skewY($a)" }
}
override fun translate(tx: CSSLengthValue) {
transformations.add { "translate($tx)" }
}
override fun translate(tx: CSSPercentageValue) {
transformations.add { "translate($tx)" }
}
override fun translate(tx: CSSLengthValue, ty: CSSLengthValue) {
transformations.add { "translate($tx, $ty)" }
}
override fun translate(tx: CSSLengthValue, ty: CSSPercentageValue) =
override fun translate(tx: CSSLengthValue, ty: CSSPercentageValue) {
transformations.add { "translate($tx, $ty)" }
}
override fun translate(tx: CSSPercentageValue, ty: CSSLengthValue) =
override fun translate(tx: CSSPercentageValue, ty: CSSLengthValue) {
transformations.add { "translate($tx, $ty)" }
}
override fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue) =
override fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue) {
transformations.add { "translate($tx, $ty)" }
}
override fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue) =
override fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue) {
transformations.add { "translate3d($tx, $ty, $tz)" }
}
override fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue) =
override fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue) {
transformations.add { "translate3d($tx, $ty, $tz)" }
}
override fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue) =
override fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue) {
transformations.add { "translate3d($tx, $ty, $tz)" }
}
override fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue) =
override fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue) {
transformations.add { "translate3d($tx, $ty, $tz)" }
}
override fun translateX(tx: CSSLengthValue) = transformations.add { "translateX($tx)" }
override fun translateX(tx: CSSPercentageValue) = transformations.add { "translateX($tx)" }
override fun translateX(tx: CSSLengthValue) {
transformations.add { "translateX($tx)" }
}
override fun translateY(ty: CSSLengthValue) = transformations.add { "translateY($ty)" }
override fun translateY(ty: CSSPercentageValue) = transformations.add { "translateY($ty)" }
override fun translateX(tx: CSSPercentageValue) {
transformations.add { "translateX($tx)" }
}
override fun translateZ(tz: CSSLengthValue) = transformations.add { "translateZ($tz)" }
override fun translateY(ty: CSSLengthValue) {
transformations.add { "translateY($ty)" }
}
override fun translateY(ty: CSSPercentageValue) {
transformations.add { "translateY($ty)" }
}
override fun translateZ(tz: CSSLengthValue) {
transformations.add { "translateZ($tz)" }
}
override fun toString(): String {
return transformations.joinToString(" ") { it.apply() }
@ -132,7 +198,7 @@ private class TransformBuilderImplementation : TransformBuilder {
}
@ExperimentalComposeWebApi
fun StyleBuilder.transform(transformFunction: TransformBuilder.() -> Unit) {
fun StyleBuilder.transform(transformContext: TransformBuilder.() -> Unit) {
val transformBuilder = TransformBuilderImplementation()
property("transform", transformBuilder.apply(transformFunction).toString())
property("transform", transformBuilder.apply(transformContext).toString())
}

108
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt

@ -1,32 +1,25 @@
package org.jetbrains.compose.web.dom
import androidx.compose.runtime.Applier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposeCompilerApi
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
import androidx.compose.runtime.ExplicitGroupsComposable
import androidx.compose.runtime.SkippableUpdater
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.remember
import org.jetbrains.compose.web.DomApplier
import org.jetbrains.compose.web.DomElementWrapper
import androidx.compose.runtime.*
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.css.StyleHolder
import org.jetbrains.compose.web.internal.runtime.DomElementWrapper
import org.jetbrains.compose.web.internal.runtime.ComposeWebInternalApi
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import org.w3c.dom.css.ElementCSSInlineStyle
import org.w3c.dom.svg.SVGElement
@OptIn(ComposeCompilerApi::class)
@Composable
@ExplicitGroupsComposable
inline fun <TScope, T, reified E : Applier<*>> ComposeDomNode(
private inline fun <TScope, T> ComposeDomNode(
noinline factory: () -> T,
elementScope: TScope,
noinline attrsSkippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
noinline content: (@Composable TScope.() -> Unit)?
) {
if (currentComposer.applier !is E) error("Invalid applier")
currentComposer.startNode()
if (currentComposer.inserting) {
currentComposer.createNode(factory)
@ -34,9 +27,7 @@ inline fun <TScope, T, reified E : Applier<*>> ComposeDomNode(
currentComposer.useNode()
}
SkippableUpdater<T>(currentComposer).apply {
attrsSkippableUpdate()
}
attrsSkippableUpdate.invoke(SkippableUpdater(currentComposer))
currentComposer.startReplaceableGroup(0x7ab4aae9)
content?.invoke(elementScope)
@ -44,48 +35,85 @@ inline fun <TScope, T, reified E : Applier<*>> ComposeDomNode(
currentComposer.endNode()
}
class DisposableEffectHolder<TElement : Element>(
var effect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null
)
@OptIn(ComposeWebInternalApi::class)
private fun DomElementWrapper.updateProperties(applicators: List<Pair<(Element, Any) -> Unit, Any>>) {
node.removeAttribute("class")
applicators.forEach { (applicator, item) ->
applicator(node, item)
}
}
@OptIn(ComposeWebInternalApi::class)
private fun DomElementWrapper.updateStyleDeclarations(styleApplier: StyleHolder) {
when (node) {
is HTMLElement, is SVGElement -> {
node.removeAttribute("style")
val style = node.unsafeCast<ElementCSSInlineStyle>().style
styleApplier.properties.forEach { (name, value) ->
style.setProperty(name, value.toString())
}
styleApplier.variables.forEach { (name, value) ->
style.setProperty(name, value.toString())
}
}
}
}
@OptIn(ComposeWebInternalApi::class)
fun DomElementWrapper.updateAttrs(attrs: Map<String, String>) {
node.getAttributeNames().forEach { name ->
if (name == "style") return@forEach
node.removeAttribute(name)
}
attrs.forEach {
node.setAttribute(it.key, it.value)
}
}
@OptIn(ComposeWebInternalApi::class)
@Composable
fun <TElement : Element> TagElement(
elementBuilder: ElementBuilder<TElement>,
applyAttrs: (AttrsBuilder<TElement>.() -> Unit)?,
content: (@Composable ElementScope<TElement>.() -> Unit)?
) {
val scope = remember { ElementScopeImpl<TElement>() }
val refEffect = remember { DisposableEffectHolder<TElement>() }
val scope = remember { ElementScopeImpl<TElement>() }
var refEffect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null
ComposeDomNode<ElementScope<TElement>, DomElementWrapper, DomApplier>(
ComposeDomNode<ElementScope<TElement>, DomElementWrapper>(
factory = {
DomElementWrapper(elementBuilder.create() as HTMLElement).also {
scope.element = it.node.unsafeCast<TElement>()
}
val node = elementBuilder.create()
scope.element = node
DomElementWrapper(node)
},
attrsSkippableUpdate = {
val attrsApplied = AttrsBuilder<TElement>().also {
if (applyAttrs != null) {
it.applyAttrs()
}
}
refEffect.effect = attrsApplied.refEffect
val attrsCollected = attrsApplied.collect()
val events = attrsApplied.collectListeners()
val attrsBuilder = AttrsBuilder<TElement>()
applyAttrs?.invoke(attrsBuilder)
refEffect = attrsBuilder.refEffect
update {
set(attrsCollected, DomElementWrapper::updateAttrs)
set(events, DomElementWrapper::updateEventListeners)
set(attrsApplied.propertyUpdates, DomElementWrapper::updateProperties)
set(attrsApplied.styleBuilder, DomElementWrapper::updateStyleDeclarations)
set(attrsBuilder.collect(), DomElementWrapper::updateAttrs)
set(attrsBuilder.collectListeners(), DomElementWrapper::updateEventListeners)
set(attrsBuilder.propertyUpdates, DomElementWrapper::updateProperties)
set(attrsBuilder.styleBuilder, DomElementWrapper::updateStyleDeclarations)
}
},
elementScope = scope,
content = content
)
DisposableEffect(null) {
refEffect.effect?.invoke(this, scope.element) ?: onDispose {}
refEffect?.let { effect ->
DisposableEffect(null) {
effect.invoke(this, scope.element)
}
}
}

6
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt

@ -4,14 +4,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposeNode
import androidx.compose.runtime.remember
import androidx.compose.web.attributes.SelectAttrsBuilder
import org.jetbrains.compose.web.DomApplier
import org.jetbrains.compose.web.DomNodeWrapper
import kotlinx.browser.document
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.attributes.builders.*
import org.jetbrains.compose.web.css.CSSRuleDeclarationList
import org.jetbrains.compose.web.css.StyleSheetBuilder
import org.jetbrains.compose.web.css.StyleSheetBuilderImpl
import org.jetbrains.compose.web.internal.runtime.DomApplier
import org.jetbrains.compose.web.internal.runtime.DomNodeWrapper
import org.jetbrains.compose.web.internal.runtime.ComposeWebInternalApi
import org.w3c.dom.Element
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLAreaElement
@ -414,6 +415,7 @@ fun Source(
)
}
@OptIn(ComposeWebInternalApi::class)
@Composable
fun Text(value: String) {
ComposeNode<DomNodeWrapper, DomApplier>(

1
web/core/src/jsTest/kotlin/CSSStylesheetTests.kt

@ -12,6 +12,7 @@ import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
object AppCSSVariables {
val width by variable<CSSUnitValue>()

1
web/core/src/jsTest/kotlin/CSSUnitApiTests.kt

@ -11,6 +11,7 @@ import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class CSSUnitApiTests {
// TODO: Cover CSS.Q, CSS.khz and CSS.hz after we'll get rid from polyfill

1
web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt

@ -8,6 +8,7 @@ import org.jetbrains.compose.web.dom.RadioInput
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.jetbrains.compose.web.testutils.*
class ControlledRadioGroupsTests {

1
web/core/src/jsTest/kotlin/DomSideEffectTests.kt

@ -5,6 +5,7 @@ import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.renderComposable
import kotlinx.browser.document
import kotlinx.dom.clear
import org.jetbrains.compose.web.testutils.*
import kotlin.test.Test
import kotlin.test.assertEquals

2
web/core/src/jsTest/kotlin/FailingTestCases.kt

@ -5,9 +5,7 @@
package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.Composable
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class FailingTestCases {

1
web/core/src/jsTest/kotlin/InlineStyleTests.kt

@ -9,6 +9,7 @@ import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class InlineStyleTests {

13
web/core/src/jsTest/kotlin/StaticComposableTests.kt

@ -10,6 +10,7 @@ import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.jetbrains.compose.web.testutils.*
class StaticComposableTests {
@Test
@ -184,12 +185,10 @@ class StaticComposableTests {
}
@Test
fun stylesPosition() {
val root = "div".asHtmlElement()
fun stylesPosition() = runTest {
val enumValues = Position.values()
renderComposable(
root = root
) {
composition {
enumValues.forEach { position ->
Span(
{
@ -197,14 +196,14 @@ class StaticComposableTests {
position(position)
}
}
) { }
)
}
}
enumValues.forEachIndexed { index, position ->
assertEquals(
"position: ${position.value};",
(root.children[index] as HTMLElement).style.cssText
nextChild().style.cssText
)
}
}

4
web/core/src/jsTest/kotlin/css/CSSBackgroundTests.kt

@ -6,13 +6,11 @@
package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class CSSBackgroundTests {
@Test

2
web/core/src/jsTest/kotlin/css/CSSBorderTests.kt

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import kotlin.test.Test

4
web/core/src/jsTest/kotlin/css/CSSBoxTests.kt

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.asHtmlElement
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test

4
web/core/src/jsTest/kotlin/css/CSSDisplayTests.kt

@ -5,14 +5,12 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.core.tests.values
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.css.value
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals

2
web/core/src/jsTest/kotlin/css/CSSFlexTests.kt

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.core.tests.values
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div

2
web/core/src/jsTest/kotlin/css/CSSListStyleTests.kt

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement

4
web/core/src/jsTest/kotlin/css/CSSMarginTests.kt

@ -6,11 +6,9 @@
package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals

4
web/core/src/jsTest/kotlin/css/CSSOverflowTests.kt

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals

4
web/core/src/jsTest/kotlin/css/CSSPaddingTests.kt

@ -6,11 +6,9 @@
package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals

5
web/core/src/jsTest/kotlin/css/CSSTextTests.kt

@ -5,12 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals

4
web/core/src/jsTest/kotlin/css/CSSUiTests.kt

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.cursor
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals

2
web/core/src/jsTest/kotlin/css/ColorTests.kt

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement

2
web/core/src/jsTest/kotlin/css/FilterTests.kt

@ -6,7 +6,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Img

4
web/core/src/jsTest/kotlin/css/GridTests.kt

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import androidx.compose.runtime.compositionLocalOf
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import kotlin.js.Json
import kotlin.test.Test
import kotlin.test.assertEquals

2
web/core/src/jsTest/kotlin/css/TransformTests.kt

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import kotlin.test.Test

55
web/core/src/jsTest/kotlin/elements/AttributesTests.kt

@ -3,6 +3,8 @@ package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import kotlinx.browser.document
import kotlinx.dom.clear
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.forId
@ -17,6 +19,7 @@ import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class AttributesTests {
@ -333,26 +336,72 @@ class AttributesTests {
assertEquals(0, refDisposeCounter)
}
@Test
fun disposableRefEffectWithChangingKey() = runTest {
var key by mutableStateOf(0)
composition {
val readKey = key // read key here to recompose an entire scope
Div(
attrs = {
id("id$readKey")
}
) {
DisposableRefEffect(readKey) {
val p = document.createElement("p").also { it.innerHTML = "Key=$readKey" }
it.appendChild(p)
onDispose {
it.clear()
}
}
}
}
assertEquals(
expected = "<div><div id=\"id0\"><p>Key=0</p></div></div>",
actual = root.outerHTML
)
key = 1
waitForRecompositionComplete()
assertEquals(
expected = "<div><div id=\"id1\"><p>Key=1</p></div></div>",
actual = root.outerHTML
)
}
@Test // issue: https://github.com/JetBrains/compose-jb/issues/981
fun attributesUpdateShouldNotCauseInlineStylesCleanUp() = runTest {
var hasValue by mutableStateOf(false)
composition {
Button(attrs = {
classes("a")
style {
color(Color.red)
}
if (hasValue) value("buttonValue")
if (hasValue) {
classes("b")
value("buttonValue")
}
}) {
Text("Button")
}
}
assertEquals("""<button style="color: red;">Button</button>""", root.innerHTML)
assertEquals(
expected = "<button class=\"a\" style=\"color: red;\">Button</button>",
actual = nextChild().outerHTML
)
hasValue = true
waitForRecompositionComplete()
assertEquals("""<button style="color: red;" value="buttonValue">Button</button>""", root.innerHTML)
assertEquals(
expected = "<button style=\"color: red;\" value=\"buttonValue\" class=\"a b\">Button</button>",
actual = currentChild().outerHTML
)
}
}

30
web/core/src/jsTest/kotlin/elements/ElementsTests.kt

@ -6,9 +6,11 @@
package org.jetbrains.compose.web.core.tests.elements
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import kotlinx.browser.document
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
@ -131,4 +133,30 @@ class ElementsTests {
assertEquals("<div><custom id=\"container\">CUSTOM</custom></div>", root.outerHTML)
}
@Test
fun elementBuilderShouldBeCalledOnce() = runTest {
var counter = 0
var flag = false
composition {
TagElement({
counter++
document.createElement("div")
}, null,
if (flag) {
{ Div() { Text("ON") } }
} else null
)
}
assertEquals(1, counter, )
flag = true
waitForRecompositionComplete()
assertEquals(1, counter)
assertEquals("<div><div>ON</div></div>", nextChild().outerHTML)
}
}

1
web/core/src/jsTest/kotlin/elements/EventTests.kt

@ -13,6 +13,7 @@ import org.w3c.dom.events.MouseEvent
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.jetbrains.compose.web.testutils.*
class EventTests {

3
web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt

@ -4,14 +4,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.core.tests.asHtmlElement
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class InputsGenerateCorrectHtmlTests {

1
web/core/src/jsTest/kotlin/elements/TableTests.kt

@ -19,6 +19,7 @@ import org.jetbrains.compose.web.dom.Tr
import org.w3c.dom.HTMLElement
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class TableTests {

4
web/gradle.properties

@ -1,6 +1,6 @@
# __LATEST_COMPOSE_RELEASE_VERSION__
COMPOSE_CORE_VERSION=0.0.0-master-build316
COMPOSE_WEB_VERSION=1.0.0-alpha1
COMPOSE_CORE_VERSION=1.0.0-alpha4-build321
COMPOSE_WEB_VERSION=1.0.0-alpha4
compose.web.buildSamples=false
compose.web.tests.integration.withFirefox
compose.web.tests.skip.benchmarks=false

33
web/internal-web-core-runtime/build.gradle.kts

@ -0,0 +1,33 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
kotlin {
js(IR) {
browser() {
testTask {
testLogging.showStandardStreams = true
useKarma {
useChromeHeadless()
useFirefox()
}
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(kotlin("stdlib-common"))
}
}
val jsMain by getting {
dependencies {
implementation(kotlin("stdlib-js"))
}
}
}
}

4
web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/ComposeWebInternalApi.kt

@ -0,0 +1,4 @@
package org.jetbrains.compose.web.internal.runtime
@RequiresOptIn("This API is internal and is likely to change in the future.")
annotation class ComposeWebInternalApi

73
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt → web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/DomApplier.kt

@ -1,14 +1,13 @@
package org.jetbrains.compose.web
package org.jetbrains.compose.web.internal.runtime
import androidx.compose.runtime.AbstractApplier
import org.jetbrains.compose.web.attributes.SyntheticEventListener
import org.jetbrains.compose.web.css.StyleHolder
import org.jetbrains.compose.web.dom.setProperty
import org.jetbrains.compose.web.dom.setVariable
import kotlinx.dom.clear
import org.w3c.dom.*
import org.w3c.dom.css.CSSStyleDeclaration
import org.w3c.dom.events.EventListener
internal class DomApplier(
@ComposeWebInternalApi
class DomApplier(
root: DomNodeWrapper
) : AbstractApplier<DomNodeWrapper>(root) {
@ -34,28 +33,14 @@ internal class DomApplier(
}
}
external interface EventListenerOptions {
var once: Boolean
var passive: Boolean
var capture: Boolean
}
internal open class DomNodeWrapper(open val node: Node) {
private var currentListeners = emptyList<SyntheticEventListener<*>>()
fun updateEventListeners(list: List<SyntheticEventListener<*>>) {
val htmlElement = node as? HTMLElement ?: return
currentListeners.forEach {
htmlElement.removeEventListener(it.event, it)
}
currentListeners = list
@ComposeWebInternalApi
interface NamedEventListener : EventListener {
val name: String
}
currentListeners.forEach {
htmlElement.addEventListener(it.event, it)
}
}
@ComposeWebInternalApi
open class DomNodeWrapper(open val node: Node) {
fun insert(index: Int, nodeWrapper: DomNodeWrapper) {
val length = node.childNodes.length
@ -88,37 +73,19 @@ internal open class DomNodeWrapper(open val node: Node) {
}
}
@ComposeWebInternalApi
class DomElementWrapper(override val node: Element): DomNodeWrapper(node) {
private var currentListeners = emptyList<NamedEventListener>()
internal class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) {
private var currentAttrs: Map<String, String>? = null
fun updateAttrs(attrs: Map<String, String>) {
currentAttrs?.forEach {
node.removeAttribute(it.key)
}
attrs.forEach {
node.setAttribute(it.key, it.value)
}
currentAttrs = attrs
}
fun updateProperties(list: List<Pair<(Element, Any) -> Unit, Any>>) {
if (node.className.isNotEmpty()) node.className = ""
list.forEach {
it.first(node, it.second)
fun updateEventListeners(list: List<NamedEventListener>) {
currentListeners.forEach {
node.removeEventListener(it.name, it)
}
}
fun updateStyleDeclarations(style: StyleHolder?) {
node.removeAttribute("style")
currentListeners = list
style?.properties?.forEach { (name, value) ->
setProperty(node.style, name, value)
}
style?.variables?.forEach { (name, value) ->
setVariable(node.style, name, value)
currentListeners.forEach {
node.addEventListener(it.name, it)
}
}
}

7
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/GlobalSnapshotManager.kt → web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/GlobalSnapshotManager.kt

@ -1,8 +1,8 @@
package org.jetbrains.compose.web
package org.jetbrains.compose.web.internal.runtime
import androidx.compose.runtime.snapshots.ObserverHandle
import androidx.compose.runtime.snapshots.Snapshot
import org.jetbrains.compose.web.GlobalSnapshotManager.ensureStarted
import org.jetbrains.compose.web.internal.runtime.GlobalSnapshotManager.ensureStarted
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@ -16,7 +16,8 @@ import kotlinx.coroutines.launch
* Composition bootstrapping mechanisms for a particular platform/framework should call
* [ensureStarted] during setup to initialize periodic global snapshot notifications.
*/
internal object GlobalSnapshotManager {
@ComposeWebInternalApi
object GlobalSnapshotManager {
private var started = false
private var commitPending = false
private var removeWriteObserver: (ObserverHandle)? = null

5
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/JsMicrotasksDispatcher.kt → web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/JsMicrotasksDispatcher.kt

@ -1,11 +1,12 @@
package org.jetbrains.compose.web
package org.jetbrains.compose.web.internal.runtime
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Runnable
import kotlin.coroutines.CoroutineContext
import kotlin.js.Promise
internal class JsMicrotasksDispatcher : CoroutineDispatcher() {
@ComposeWebInternalApi
class JsMicrotasksDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
Promise.resolve(Unit).then { block.run() }
}

2
web/settings.gradle.kts

@ -37,6 +37,8 @@ fun module(name: String, path: String) {
module(":web-core", "core")
module(":web-widgets", "widgets")
module(":compose-compiler-integration", "compose-compiler-integration")
module(":internal-web-core-runtime", "internal-web-core-runtime")
module(":test-utils", "test-utils")
if (extra["compose.web.tests.skip.benchmarks"]!!.toString().toBoolean() != true) {
module(":web-benchmark-core", "benchmark-core")

42
web/test-utils/build.gradle.kts

@ -0,0 +1,42 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
repositories {
mavenCentral()
}
kotlin {
js(IR) {
browser() {
testTask {
testLogging.showStandardStreams = true
useKarma {
useChromeHeadless()
useFirefox()
}
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib-common"))
}
}
val jsMain by getting {
dependencies {
implementation(project(":internal-web-core-runtime"))
implementation(kotlin("stdlib-js"))
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
}
}

4
web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/ComposeWebExperimentalTestsApi.kt

@ -0,0 +1,4 @@
package org.jetbrains.compose.web.testutils
@RequiresOptIn("This API is experimental and is likely to change in the future.")
annotation class ComposeWebExperimentalTestsApi

70
web/core/src/jsTest/kotlin/TestUtils.kt → web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt

@ -1,17 +1,14 @@
package org.jetbrains.compose.web.core.tests
package org.jetbrains.compose.web.testutils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MonotonicFrameClock
import org.jetbrains.compose.web.renderComposable
import androidx.compose.runtime.*
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.promise
import kotlinx.dom.clear
import org.jetbrains.compose.web.internal.runtime.*
import org.w3c.dom.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.time.DurationUnit
@ -23,6 +20,7 @@ import kotlin.time.toDuration
* There is no need to create its instances manually.
* @see [runTest]
*/
@ComposeWebExperimentalTestsApi
class TestScope : CoroutineScope by MainScope() {
/**
@ -31,7 +29,7 @@ class TestScope : CoroutineScope by MainScope() {
*/
val root = "div".asHtmlElement()
private val recompositionCompleteEventsChannel = Channel<Unit>()
private var waitForRecompositionCompleteContinuation: Continuation<Unit>? = null
private val childrenIterator = root.children.asList().listIterator()
init {
@ -39,28 +37,58 @@ class TestScope : CoroutineScope by MainScope() {
}
private fun onRecompositionComplete() {
launch {
recompositionCompleteEventsChannel.send(Unit)
}
waitForRecompositionCompleteContinuation?.resume(Unit)
waitForRecompositionCompleteContinuation = null
}
/**
* Cleans up the [root] content.
* Creates a new composition with a given Composable [content].
*/
@ComposeWebExperimentalTestsApi
fun composition(content: @Composable () -> Unit) {
root.clear()
renderComposable(
root = root,
monotonicFrameClock = TestMonotonicClockImpl(
onRecomposeComplete = this::onRecompositionComplete
)
) {
renderTestComposable(root = root) {
content()
}
}
/**
* Use this method to test the composition mounted at [root]
*
* @param root - the [Element] that will be the root of the DOM tree managed by Compose
* @param content - the Composable lambda that defines the composition content
*
* @return the instance of the [Composition]
*/
@OptIn(ComposeWebInternalApi::class)
@ComposeWebExperimentalTestsApi
fun <TElement : Element> renderTestComposable(
root: TElement,
content: @Composable () -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
val context = TestMonotonicClockImpl(
onRecomposeComplete = this::onRecompositionComplete
) + JsMicrotasksDispatcher()
val recomposer = Recomposer(context)
val composition = ControlledComposition(
applier = DomApplier(DomNodeWrapper(root)),
parent = recomposer
)
composition.setContent @Composable {
content()
}
CoroutineScope(context).launch(start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
return composition
}
/**
* @return a reference to the next child element of the root.
* Subsequent calls will return next child reference every time.
@ -104,7 +132,9 @@ class TestScope : CoroutineScope by MainScope() {
* Suspends until recomposition completes.
*/
suspend fun waitForRecompositionComplete() {
recompositionCompleteEventsChannel.receive()
suspendCoroutine<Unit> { continuation ->
waitForRecompositionCompleteContinuation = continuation
}
}
}
@ -140,11 +170,13 @@ class TestScope : CoroutineScope by MainScope() {
* }
* ```
*/
@ComposeWebExperimentalTestsApi
fun runTest(block: suspend TestScope.() -> Unit): dynamic {
val scope = TestScope()
return scope.promise { block(scope) }
}
@ComposeWebExperimentalTestsApi
fun String.asHtmlElement() = document.createElement(this) as HTMLElement
private object MutationObserverOptions : MutationObserverInit {

82
web/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt

@ -0,0 +1,82 @@
import androidx.compose.runtime.RecomposeScope
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentRecomposeScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.testutils.ComposeWebExperimentalTestsApi
import org.jetbrains.compose.web.testutils.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
@OptIn(ComposeWebExperimentalTestsApi::class)
class TestsForTestUtils {
@Test
fun waitForRecompositionComplete_suspends_and_continues_properly() = runTest {
var recomposeScope: RecomposeScope? = null
composition {
recomposeScope = currentRecomposeScope
}
delay(100) // to let the initial composition complete
var waitForRecompositionCompleteContinued = false
val job = launch {
waitForRecompositionComplete()
waitForRecompositionCompleteContinued = true
}
delay(100) // to check that `waitForRecompositionComplete` is suspended after delay
assertEquals(false, waitForRecompositionCompleteContinued)
delay(100)
// we made no changes during 100 ms, so `waitForRecompositionComplete` should remain suspended
assertEquals(false, waitForRecompositionCompleteContinued)
recomposeScope!!.invalidate() // force recomposition
job.join()
assertEquals(true, waitForRecompositionCompleteContinued)
}
@Test
fun waitForChanges_suspends_and_continues_properly() = runTest {
var waitForChangesContinued = false
var recomposeScope: RecomposeScope? = null
var showText = ""
composition {
recomposeScope = currentRecomposeScope
SideEffect {
root.innerText = showText
}
}
assertEquals("<div></div>", root.outerHTML)
val job = launch {
waitForChanges(root)
waitForChangesContinued = true
}
delay(100) // to check that `waitForChanges` is suspended after delay
assertEquals(false, waitForChangesContinued)
// force recomposition and check that `waitForChanges` remains suspended as no changes occurred
recomposeScope!!.invalidate()
waitForRecompositionComplete()
assertEquals(false, waitForChangesContinued)
// Make changes and check that `waitForChanges` continues
showText = "Hello World!"
recomposeScope!!.invalidate()
waitForRecompositionComplete()
job.join()
assertEquals(true, waitForChangesContinued)
assertEquals("<div>Hello World!</div>", root.outerHTML)
}
}
Loading…
Cancel
Save