Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web: Add Canvas element #1823

Merged
merged 1 commit into from Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,9 +1,11 @@
package org.jetbrains.compose.web.attributes

import org.jetbrains.compose.web.attributes.builders.saveControlledInputState
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.events.SyntheticSubmitEvent
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLImageElement
import org.w3c.dom.HTMLInputElement
Expand Down Expand Up @@ -329,3 +331,9 @@ fun AttrsScope<HTMLTableCellElement>.colspan(value: Int): AttrsScope<HTMLTableCe
fun AttrsScope<HTMLTableCellElement>.rowspan(value: Int): AttrsScope<HTMLTableCellElement> =
attr("rowspan", value.toString())

/* Canvas attributes */
fun AttrsScope<HTMLCanvasElement>.width(value: CSSSizeValue<out CSSUnitLengthOrPercentage>) =
attr("width", value.toString())

fun AttrsScope<HTMLCanvasElement>.height(value: CSSSizeValue<out CSSUnitLengthOrPercentage>) =
attr("height", value.toString())
Expand Up @@ -17,6 +17,7 @@ import org.w3c.dom.HTMLAreaElement
import org.w3c.dom.HTMLAudioElement
import org.w3c.dom.HTMLBRElement
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLDataListElement
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
Expand Down Expand Up @@ -93,6 +94,7 @@ private val Object: ElementBuilder<HTMLObjectElement> = ElementBuilderImplementa
private val Param: ElementBuilder<HTMLParamElement> = ElementBuilderImplementation("param")
private val Picture: ElementBuilder<HTMLPictureElement> = ElementBuilderImplementation("picture")
private val Source: ElementBuilder<HTMLSourceElement> = ElementBuilderImplementation("source")
private val Canvas: ElementBuilder<HTMLCanvasElement> = ElementBuilderImplementation("canvas")

private val Div: ElementBuilder<HTMLDivElement> = ElementBuilderImplementation("div")
private val A: ElementBuilder<HTMLAnchorElement> = ElementBuilderImplementation("a")
Expand Down Expand Up @@ -414,6 +416,18 @@ fun Source(
)
}

@Composable
fun Canvas(
attrs: AttrBuilderContext<HTMLCanvasElement>? = null,
content: ContentBuilder<HTMLCanvasElement>? = null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContentBuilder is a @Composable lambda. That means it would be possible to write some code like this:

Canvas {
    Div {}
}

I'm not sure that @composable content is applicable for Canvas. I was thinking that canvas' content can be managed only in DisposableEffect or some side effect (at least for now). Or do I miss something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to MDN you can provide innerHTML content inside a canvas: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas
But mostly for static content as fallback when JS is disabled or the browser does not support canvas. But Compose needs JS and all modern browser (even Internet Explorer :) support canvas. So indeed, this could be removed. Maybe it could take the DisposableEffect as parameter instead:

// current:
Canvas({
  ref {
      Chart(it, jso { })
      onDispose { }
  }
})

// new
Canvas {
  Chart(it, jso { })
  onDispose { }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW ContentBuilder is required, otherwise you can't use a composable side effect with a reference to the canvas htmlelement. DisposableEffect is not composable, but DOMSideEffect is.

var chartData by remember { mutableStateOf<ChartData>() }
Canvas({}) {
    DomSideEffect(chartData) {
        val chart = Chart(it, config(chartData))
        onDispose {
            chart.destroy()
        }
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DisposableEffect is composable - https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#DisposableEffect(kotlin.Function1)

I think it's worth to allow everyone use as many effects as they want inside Canvas {}, so I agree that content is needed for this.
I still don't want allow calling Div {} or any html element within canvas content. But I don't see a way to prohibit it now.

Thanks! lgtm

Copy link
Contributor Author

@hfhbd hfhbd Feb 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did I miss something?

* [ref] can be used to retrieve a reference to a html element.
* The lambda that `ref` takes in is not Composable. It will be called only once when an element added into a composition.
* Likewise, the lambda passed in `onDispose` will be called only once when an element leaves the composition.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's right fun ref(...) is a function in AttrsScope and it's not composable https://github.com/JetBrains/compose-jb/blob/7ea30be92404ef4dbdd679cca0b88970b6f3b12e/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsScope.kt#L82

In your comment above you mentioned that "DisposableEffect is not composable", so I posted the link to docs where it shows that fun DisposableEffect(...) has composable annotation.

Anyway, you're right, one can't use any composable function within ref {} lambda.

) {
TagElement(
elementBuilder = Canvas,
applyAttrs = attrs,
content = content
)
}

@OptIn(ComposeWebInternalApi::class)
@Composable
fun Text(value: String) {
Expand Down
11 changes: 11 additions & 0 deletions web/core/src/jsTest/kotlin/elements/AttributesTests.kt
Expand Up @@ -550,4 +550,15 @@ class AttributesTests {
check(InputMode.Search, "search")
check(InputMode.Url, "url")
}

@Test
fun canvasAttributeTest() = runTest {
composition {
Canvas({
height(400.px)
width(400.px)
})
}
assertEquals("""<canvas height="400px" width="400px"></canvas>""",root.innerHTML)
}
}
1 change: 1 addition & 0 deletions web/core/src/jsTest/kotlin/elements/ElementsTests.kt
Expand Up @@ -47,6 +47,7 @@ class ElementsTests {
Pair({ Param() }, "PARAM"),
Pair({ Picture() }, "PICTURE"),
Pair({ Source() }, "SOURCE"),
Pair({ Canvas() }, "CANVAS"),

Pair({ Div() }, "DIV"),
Pair({ A() }, "A"),
Expand Down