Skip to content

Commit

Permalink
CfW: let ComposeWindow accept a custom canvas id (html canvas element…
Browse files Browse the repository at this point in the history
… id) (#626)

* CfW: let ComposeWindow accept a custom canvas id (html canvas element id)

It upstreams the change from wasm-main branch to jb-main branch

* PR review fixed
  • Loading branch information
eymar authored Jul 7, 2023
1 parent 8390da2 commit f05c388
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package androidx.compose.mpp.demo

import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
import androidx.compose.ui.window.Window
import kotlinx.browser.document
import org.jetbrains.skiko.GenericSkikoView
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.wasm.onWasmReady
import org.w3c.dom.HTMLCanvasElement

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
onWasmReady {
Window("Compose/JS sample") {
CanvasBasedWindow("Compose/JS sample", canvasElementId = "canvas1") {
val app = remember { App() }
app.Content()
}
Expand Down
11 changes: 10 additions & 1 deletion compose/mpp/demo/src/jsMain/resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@
<title>compose multiplatform web demo</title>
<script src="skiko.js"> </script>
<link type="text/css" rel="stylesheet" href="styles.css">
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>

<body>
<h1>compose multiplatform web demo</h1>
<div>
<canvas id="ComposeTarget" width="800" height="600"></canvas>
<canvas id="canvas1" width="800" height="600"></canvas>
</div>
<script src="demo.js"> </script>
</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 The Android Open Source Project
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,19 +17,28 @@
package androidx.compose.ui.window

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.createSkiaLayer
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.native.ComposeLayer
import androidx.compose.ui.platform.JSTextInputService
import androidx.compose.ui.platform.Platform
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.isActive
import org.w3c.dom.HTMLCanvasElement
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
import kotlinx.coroutines.delay

internal actual class ComposeWindow actual constructor() {
internal actual class ComposeWindow(val canvasId: String) {

actual constructor(): this(defaultCanvasElementId)

private val density: Density = Density(
density = window.devicePixelRatio.toFloat(),
Expand All @@ -53,14 +62,22 @@ internal actual class ComposeWindow actual constructor() {
input = jsTextInputService.input
)

// TODO: generalize me.
val canvas = document.getElementById("ComposeTarget") as HTMLCanvasElement
val canvas = document.getElementById(canvasId) as HTMLCanvasElement

init {
layer.layer.attachTo(canvas)
canvas.setAttribute("tabindex", "0")
layer.layer.needRedraw()

layer.setSize(canvas.width, canvas.height)
}

fun resize(newSize: IntSize) {
canvas.width = newSize.width
canvas.height = newSize.height
layer.layer.attachTo(canvas)
layer.setSize(canvas.width, canvas.height)
layer.layer.needRedraw()
}

/**
Expand All @@ -82,3 +99,64 @@ internal actual class ComposeWindow actual constructor() {
layer.dispose()
}
}

private val defaultCanvasElementId = "ComposeTarget"

@ExperimentalComposeUiApi
/**
* EXPERIMENTAL! Might be deleted or changed in the future!
*
* Initializes the composition in HTML canvas identified by [canvasElementId].
*
* It can be resized by providing [requestResize].
* By default, it will listen to the window resize events.
*/
fun CanvasBasedWindow(
title: String = "JetpackNativeWindow",
canvasElementId: String = defaultCanvasElementId,
requestResize: (suspend () -> IntSize)? = null,
content: @Composable () -> Unit = { }
) {

val actualRequestResize: suspend () -> IntSize = if (requestResize != null) {
requestResize
} else {
// we use Channel instead of suspendCancellableCoroutine,
// because we want to drop old resize events
val channel = Channel<IntSize>(capacity = CONFLATED)

// we subscribe to 'resize' only once and never unsubscribe,
// because the default behaviour expects that the Canvas takes the entire window space,
// so the app has the same lifecycle as the browser tab.
window.addEventListener("resize", { _ ->
val w = document.documentElement?.clientWidth ?: 0
val h = document.documentElement?.clientHeight ?: 0
channel.trySend(IntSize(w, h))
})

suspend {
channel.receive()
}
}

if (requestResize == null) {
(document.getElementById(canvasElementId) as? HTMLCanvasElement)?.let {
it.width = document.documentElement?.clientWidth ?: 0
it.height = document.documentElement?.clientHeight ?: 0
}
}

ComposeWindow(canvasId = canvasElementId).apply {
val composeWindow = this
setContent {
content()
LaunchedEffect(Unit) {
while (isActive) {
val newSize = actualRequestResize()
composeWindow.resize(newSize)
delay(100) // throttle
}
}
}
}
}

0 comments on commit f05c388

Please sign in to comment.