From 095379d513511fadc1179e6b64fae5b3c69ff503 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Fri, 22 Oct 2021 12:27:18 +0200 Subject: [PATCH] Introduce basic support for SVG (#1289) --- .../org/jetbrains/compose/ComposePlugin.kt | 4 + web/settings.gradle.kts | 1 + web/svg/build.gradle.kts | 49 ++ .../web/ExperimentalComposeWebSvgApi.kt | 9 + .../org/jetbrains/compose/web/svg/svg.kt | 729 ++++++++++++++++++ web/svg/src/jsTest/kotlin/svg/SvgTests.kt | 453 +++++++++++ .../compose/web/testutils/TestUtils.kt | 1 + 7 files changed, 1246 insertions(+) create mode 100644 web/svg/build.gradle.kts create mode 100644 web/svg/src/jsMain/kotlin/org/jetbrains/compose/web/ExperimentalComposeWebSvgApi.kt create mode 100644 web/svg/src/jsMain/kotlin/org/jetbrains/compose/web/svg/svg.kt create mode 100644 web/svg/src/jsTest/kotlin/svg/SvgTests.kt diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt index 2d1b618bc4..8e6f0caab6 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt @@ -197,6 +197,10 @@ class ComposePlugin : Plugin { composeDependency("org.jetbrains.compose.web:web-core") } + val svg by lazy { + composeDependency("org.jetbrains.compose.web:web-svg") + } + val widgets by lazy { composeDependency("org.jetbrains.compose.web:web-widgets") } diff --git a/web/settings.gradle.kts b/web/settings.gradle.kts index 3535906f97..f3d630c0f6 100644 --- a/web/settings.gradle.kts +++ b/web/settings.gradle.kts @@ -35,6 +35,7 @@ fun module(name: String, path: String) { module(":web-core", "core") +module(":web-svg", "svg") module(":web-widgets", "widgets") module(":web-integration-core", "integration-core") module(":web-integration-widgets", "integration-widgets") diff --git a/web/svg/build.gradle.kts b/web/svg/build.gradle.kts new file mode 100644 index 0000000000..31e49f0cf7 --- /dev/null +++ b/web/svg/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + + +kotlin { + js(IR) { + browser() { + testTask { + testLogging.showStandardStreams = true + useKarma { + useChromeHeadless() + } + } + } + binaries.executable() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(compose.runtime) + implementation(kotlin("stdlib-common")) + } + } + + val jsMain by getting { + dependencies { + implementation(project(":internal-web-core-runtime")) + implementation(kotlin("stdlib-js")) + implementation(project(":web-core")) + } + } + + val jsTest by getting { + dependencies { + implementation(project(":test-utils")) + implementation(kotlin("test-js")) + } + } + + all { + languageSettings { + useExperimentalAnnotation("org.jetbrains.compose.web.testutils.ComposeWebExperimentalTestsApi") + } + } + } +} diff --git a/web/svg/src/jsMain/kotlin/org/jetbrains/compose/web/ExperimentalComposeWebSvgApi.kt b/web/svg/src/jsMain/kotlin/org/jetbrains/compose/web/ExperimentalComposeWebSvgApi.kt new file mode 100644 index 0000000000..3431ceed81 --- /dev/null +++ b/web/svg/src/jsMain/kotlin/org/jetbrains/compose/web/ExperimentalComposeWebSvgApi.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web + +@RequiresOptIn("This API is experimental and is likely to change in the future.") +annotation class ExperimentalComposeWebSvgApi diff --git a/web/svg/src/jsMain/kotlin/org/jetbrains/compose/web/svg/svg.kt b/web/svg/src/jsMain/kotlin/org/jetbrains/compose/web/svg/svg.kt new file mode 100644 index 0000000000..b4ebaf7c41 --- /dev/null +++ b/web/svg/src/jsMain/kotlin/org/jetbrains/compose/web/svg/svg.kt @@ -0,0 +1,729 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.svg + +import androidx.compose.runtime.Composable +import kotlinx.browser.document +import org.jetbrains.compose.web.ExperimentalComposeWebSvgApi +import org.jetbrains.compose.web.css.CSSLengthOrPercentageValue +import org.jetbrains.compose.web.dom.* +import org.w3c.css.masking.SVGClipPathElement +import org.w3c.css.masking.SVGMaskElement +import org.w3c.dom.Element +import org.w3c.dom.svg.* + +private open class ElementBuilderNS(private val tagName: String, private val namespace: String) : + ElementBuilder { + private val el: Element by lazy { document.createElementNS(namespace, tagName) } + override fun create(): TElement = el.cloneNode().unsafeCast() +} + +const val SVG_NS = "http://www.w3.org/2000/svg" + +private val A = ElementBuilderNS("a", SVG_NS) +private val Animate = ElementBuilderNS("animate", SVG_NS) +private val AnimateMotion = ElementBuilderNS("animateMotion", SVG_NS) +private val AnimateTransform = ElementBuilderNS("animateTransform", SVG_NS) +private val Circle = ElementBuilderNS("circle", SVG_NS) +private val ClipPath = ElementBuilderNS("clipPath", SVG_NS) +private val Defs = ElementBuilderNS("defs", SVG_NS) +private val Desc = ElementBuilderNS("desc", SVG_NS) +private val Ellipse = ElementBuilderNS("ellipse", SVG_NS) +private val Filter = ElementBuilderNS("filter", SVG_NS) +private val G = ElementBuilderNS("g", SVG_NS) +private val Image = ElementBuilderNS("image", SVG_NS) +private val Line = ElementBuilderNS("line", SVG_NS) +private val LinearGradient = ElementBuilderNS("linearGradient", SVG_NS) +private val Marker = ElementBuilderNS("marker", SVG_NS) +private val Mask = ElementBuilderNS("mask", SVG_NS) +private val Mpath = ElementBuilderNS("mpath", SVG_NS) +private val Path = ElementBuilderNS("path", SVG_NS) +private val Pattern = ElementBuilderNS("pattern", SVG_NS) +private val Polygon = ElementBuilderNS("polygon", SVG_NS) +private val Polyline = ElementBuilderNS("polyline", SVG_NS) +private val RadialGradient = ElementBuilderNS("radialGradient", SVG_NS) +private val Rect = ElementBuilderNS("rect", SVG_NS) +private val Set = ElementBuilderNS("set", SVG_NS) +private val Stop = ElementBuilderNS("stop", SVG_NS) +private val Svg = ElementBuilderNS("svg", SVG_NS) +private val Switch = ElementBuilderNS("switch", SVG_NS) +private val Symbol = ElementBuilderNS("symbol", SVG_NS) +private val Text = ElementBuilderNS("text", SVG_NS) +private val TextPath = ElementBuilderNS("textPath", SVG_NS) +private val Title = ElementBuilderNS("title", SVG_NS) +private val Tspan = ElementBuilderNS("tspan", SVG_NS) +private val Use = ElementBuilderNS("use", SVG_NS) +private val View = ElementBuilderNS("view", SVG_NS) + +@Composable +@ExperimentalComposeWebSvgApi +fun Svg( + viewBox: String? = null, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Svg, + applyAttrs = { + viewBox?.let { attr("viewBox", it) } + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.SvgA( + href: String, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = A, + applyAttrs = { + attr("href", href) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Circle( + cx: CSSLengthOrPercentageValue, + cy: CSSLengthOrPercentageValue, + r: CSSLengthOrPercentageValue, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Circle, + applyAttrs = { + attr("cx", cx.toString()) + attr("cy", cy.toString()) + attr("r", r.toString()) + attrs?.invoke(this) + }, + content = content + ) +} + + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Circle( + cx: Number, + cy: Number, + r: Number, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Circle, + applyAttrs = { + attr("cx", cx.toString()) + attr("cy", cy.toString()) + attr("r", r.toString()) + attrs?.invoke(this) + }, + content = content + ) +} + + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.SvgText( + text: String, + x: Number = 0, + y: Number = 0, + attrs: AttrBuilderContext? = null, +) { + TagElement( + elementBuilder = Text, + applyAttrs = { + attr("x", x.toString()) + attr("y", y.toString()) + attrs?.invoke(this) + }, + content = { + Text(text) + } + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.View( + id: String, + viewBox: String, + attrs: AttrBuilderContext? = null, +) { + TagElement( + elementBuilder = View, + applyAttrs = { + attr("id", id) + attr("viewBox", viewBox) + attrs?.invoke(this) + }, + content = null + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Rect( + x: Number, + y: Number, + width: Number, + height: Number, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Rect, + applyAttrs = { + attr("x", x.toString()) + attr("y", y.toString()) + attr("width", width.toString()) + attr("height", height.toString()) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Rect( + x: CSSLengthOrPercentageValue, + y: CSSLengthOrPercentageValue, + width: CSSLengthOrPercentageValue, + height: CSSLengthOrPercentageValue, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Rect, + applyAttrs = { + attr("x", x.toString()) + attr("y", y.toString()) + attr("width", width.toString()) + attr("height", height.toString()) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Ellipse( + cx: CSSLengthOrPercentageValue, + cy: CSSLengthOrPercentageValue, + rx: CSSLengthOrPercentageValue, + ry: CSSLengthOrPercentageValue, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Ellipse, + applyAttrs = { + attr("cx", cx.toString()) + attr("cy", cy.toString()) + attr("rx", rx.toString()) + attr("ry", ry.toString()) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Ellipse( + cx: Number, + cy: Number, + rx: Number, + ry: Number, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Ellipse, + applyAttrs = { + attr("cx", cx.toString()) + attr("cy", cy.toString()) + attr("rx", rx.toString()) + attr("ry", ry.toString()) + attrs?.invoke(this) + }, + content = content + ) +} + + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Symbol( + id: String? = null, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Symbol, + applyAttrs = { + id?.let { attr("id", it) } + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Use( + href: String, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Use, + applyAttrs = { + attr("href", href) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Line( + x1: CSSLengthOrPercentageValue, + y1: CSSLengthOrPercentageValue, + x2: CSSLengthOrPercentageValue, + y2: CSSLengthOrPercentageValue, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Line, + applyAttrs = { + attr("x1", x1.toString()) + attr("y1", y1.toString()) + attr("x2", x2.toString()) + attr("y2", y2.toString()) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Line( + x1: Number, + y1: Number, + x2: Number, + y2: Number, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Line, + applyAttrs = { + attr("x1", x1.toString()) + attr("y1", y1.toString()) + attr("x2", x2.toString()) + attr("y2", y2.toString()) + attrs?.invoke(this) + }, + content = content + ) +} + + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.ClipPath( + id: String, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = ClipPath, + applyAttrs = { + attr("id", id) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Path( + d: String, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Path, + applyAttrs = { + attr("d", d) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.G( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = G, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Image( + href: String, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Image, + applyAttrs = { + attr("href", href) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Mask( + id: String? = null, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Mask, + applyAttrs = { + id?.let { attr("id", it) } + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Defs( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Defs, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Pattern( + id: String, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Pattern, + applyAttrs = { + attr("id", id) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Polygon( + vararg points: Number, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Polygon, + applyAttrs = { + attr("points", points.toList().chunked(2).joinToString(" ") { it.joinToString(",") }) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Polyline( + vararg points: Number, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Polyline, + applyAttrs = { + attr("points", points.toList().chunked(2).joinToString(" ") { it.joinToString(",") }) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.TextPath( + href: String, + text: String, + attrs: AttrBuilderContext? = null, +) { + TagElement( + elementBuilder = TextPath, + applyAttrs = { + attr("href", href) + attrs?.invoke(this) + }, + content = { + Text(text) + } + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Animate( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Animate, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.AnimateMotion( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = AnimateMotion, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.AnimateTransform( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = AnimateTransform, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.LinearGradient( + id: String? = null, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = LinearGradient, + applyAttrs = { + id?.let { attr("id", it) } + attrs?.invoke(this) + }, + content = content + ) +} + + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.RadialGradient( + id: String? = null, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = RadialGradient, + applyAttrs = { + id?.let { attr("id", it) } + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Stop( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Stop, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Switch( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Switch, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Title( + text: String, + attrs: AttrBuilderContext? = null, +) { + TagElement( + elementBuilder = Title, + applyAttrs = attrs, + content = { + Text(text) + } + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Tspan( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Tspan, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Desc( + content: String, + attrs: AttrBuilderContext? = null, +) { + TagElement( + elementBuilder = Desc, + applyAttrs = attrs, + content = { + Text(content) + } + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Marker( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Marker, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Mpath( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Mpath, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Filter( + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Filter, + applyAttrs = attrs, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun ElementScope.Set( + attributeName: String, + to: String, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = Set, + applyAttrs = { + attr("attributeName", attributeName) + attr("to", to) + attrs?.invoke(this) + }, + content = content + ) +} + +@Composable +@ExperimentalComposeWebSvgApi +fun SvgElement( + name: String, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null +) { + TagElement( + elementBuilder = ElementBuilderNS(name, SVG_NS), + applyAttrs = attrs, + content = content + ) +} \ No newline at end of file diff --git a/web/svg/src/jsTest/kotlin/svg/SvgTests.kt b/web/svg/src/jsTest/kotlin/svg/SvgTests.kt new file mode 100644 index 0000000000..7b5c5420c2 --- /dev/null +++ b/web/svg/src/jsTest/kotlin/svg/SvgTests.kt @@ -0,0 +1,453 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.core.tests.svg + +import org.jetbrains.compose.web.ExperimentalComposeWebSvgApi +import org.jetbrains.compose.web.css.opacity +import org.jetbrains.compose.web.css.percent +import org.jetbrains.compose.web.css.px +import org.jetbrains.compose.web.svg.* +import org.jetbrains.compose.web.testutils.* +import org.w3c.dom.svg.SVGCircleElement +import kotlin.test.Test +import kotlin.test.assertEquals + +@ExperimentalComposeWebSvgApi +class SvgTests { + @Test + fun nodeNames() = runTest { + composition { + Svg { + Animate() + AnimateMotion() + AnimateTransform() + Defs() + Filter() + G() + Marker() + Mpath() + Switch() + Tspan() + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun clipPathTest() = runTest { + composition { + Svg { + ClipPath("myClip") { + Circle(40.px, 35.px, 36.px) + } + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun maskTest() = runTest { + composition { + Svg { + Mask("myMask") + } + } + + assertEquals("", nextChild().outerHTML) + } + + + @Test + fun svgATest() = runTest { + composition { + Svg { + SvgA("/docs/Web/SVG/Element/circle", { + attr("target", "_blank") + }) + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun descTest() = runTest { + composition { + Svg { + Desc("some description", { attr("id", "myDesc") }) + } + } + + assertEquals("some description", nextChild().outerHTML) + } + + @Test + fun setTest() = runTest { + composition { + Svg { + Rect(0, 0, 10, 10, { attr("id", "rect") }) { + Set(attributeName = "class", to = "round", { attr("begin", "me.click"); attr("dur", "2s") }) + } + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + + @Test + fun titleTest() = runTest { + composition { + Svg { + Rect(10, 20, 30, 30) { + Title("some title") + } + } + } + + assertEquals( + "some title", + nextChild().outerHTML + ) + } + + @Test + fun svgTextTest() = runTest { + composition { + Svg { + SvgText("some text", 20, 30, { + classes("small") + }) + } + } + + assertEquals( + "some text", + nextChild().outerHTML + ) + } + + @Test + fun textPathTest() = runTest { + composition { + Svg { + TextPath("#someHref", "Some text") + } + } + + assertEquals( + "Some text", + nextChild().outerHTML + ) + } + + + @Test + fun ellipseTest() = runTest { + composition { + Svg { + Ellipse(50, 60, 70, 20, { + attr("color", "yellow") + }) + Ellipse(50.px, 60.px, 70.percent, 20.px, { + attr("color", "red") + }) + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + + @Test + fun circleTest() = runTest { + composition { + Svg { + Circle(50, 60, 70, { + attr("color", "red") + }) + Circle(50.px, 60.px, 70.percent, { + attr("color", "red") + }) + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun rectTest() = runTest { + composition { + Svg { + Rect(0, 20, 100, 200, { + attr("color", "red") + }) + Rect(0.px, 20.px, 100.px, 200.px, { + attr("color", "red") + }) + + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + + @Test + fun imageTest() = runTest { + composition { + Svg { + Image("/image.png", { + attr("preserveAspectRatio", "xMidYMid meet") + }) + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun lineTest() = runTest { + composition { + Svg { + Line(0, 80, 100, 20, { + attr("stroke", "red") + }) + Line(0.px, 80.px, 100.px, 20.px, { + attr("stroke", "black") + }) + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun polylineTest() = runTest { + composition { + Svg { + Polyline(0, 100, 50, 25, 50, 75, 100, 0, attrs = { + attr("stroke", "red") + }) + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + + @Test + fun polygonTest() = runTest { + composition { + Svg { + Polygon(0, 100, 50, 25, 50, 75, 100, 0, attrs = { + attr("stroke", "red") + }) + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun linearGradientTest() = runTest { + composition { + Svg { + LinearGradient("myGradient") { + Stop({ + attr("offset", 10.percent.toString()) + attr("stop-color", "gold") + }) + Stop({ + attr("offset", 95.percent.toString()) + attr("stop-color", "red") + }) + } + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun radialGradientTest() = runTest { + composition { + Svg { + RadialGradient("myGradient") { + Stop({ + attr("offset", 10.percent.toString()) + attr("stop-color", "gold") + }) + Stop({ + attr("offset", 95.percent.toString()) + attr("stop-color", "red") + }) + } + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + + @Test + fun patternTest() = runTest { + composition { + Svg { + Pattern("something") { + Polygon(0, 100, 50, 25, 50, 75, 100, 0, attrs = { + attr("stroke", "red") + }) + } + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun viewTest() = runTest { + composition { + Svg { + View("one", "0 0 100 100") + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + + @Test + fun pathTest() = runTest { + composition { + Svg { + Path( + """ + M 10,30 + A 20,20 0,0,1 50,30 + A 20,20 0,0,1 90,30 + Q 90,60 50,90 + Q 10,60 10,30 z + """.trimIndent() + ) + } + } + + assertEquals( + "", nextChild().outerHTML + ) + } + + @Test + fun useTest() = runTest { + composition { + Svg { + Symbol("myDot", { + attr("width", "10") + attr("height", "10") + attr("viewBox", "0 0 2 2") + }) { + Circle(1.px, 1.px, 1.px) + } + + Use("myDot", { + attr("x", "5") + attr("y", "5") + style { + opacity(1) + } + }) + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun svgElementTest() = runTest { + composition { + Svg { + SvgElement("circle", { + attr("cx", 12.px.toString()) + attr("cy", 22.px.toString()) + attr("r", 5.percent.toString()) + }) + } + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + + @Test + fun svgElementWithViewBoxTest() = runTest { + composition { + Svg(viewBox = "0 0 200 200") + } + + assertEquals( + "", + nextChild().outerHTML + ) + } + +} diff --git a/web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt b/web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt index 0ae538a87e..7a82b3e1d7 100644 --- a/web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt +++ b/web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt @@ -94,6 +94,7 @@ class TestScope : CoroutineScope by MainScope() { * Subsequent calls will return next child reference every time. */ fun nextChild() = childrenIterator.next() as HTMLElement + fun nextChild() = childrenIterator.next() as T /** * @return a reference to current child.