Oliver.O
08/09/2022, 10:16 PMBrowserViewportWindow for the initial window. It is easy to use, supports resizing, fills the entire browser viewport, sets the window title correctly and does not require a stylesheet. Inspired by code in @Tlaster's PreCompose. If you want to try: Code in 🧵Oliver.O
08/09/2022, 10:17 PM@file:Suppress(
"INVISIBLE_MEMBER",
"INVISIBLE_REFERENCE",
"EXPOSED_PARAMETER_TYPE"
) // WORKAROUND: ComposeWindow and ComposeLayer are internal
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.ComposeWindow
import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLStyleElement
import org.w3c.dom.HTMLTitleElement
private const val CANVAS_ELEMENT_ID = "ComposeTarget" // Hardwired into ComposeWindow
/**
* A Skiko/Canvas-based top-level window using the browser's entire viewport. Supports resizing.
*/
fun BrowserViewportWindow(
title: String = "Untitled",
content: @Composable ComposeWindow.() -> Unit
) {
val htmlHeadElement = document.head!!
htmlHeadElement.appendChild(
(document.createElement("style") as HTMLStyleElement).apply {
type = "text/css"
appendChild(
document.createTextNode(
"""
html, body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
}
#$CANVAS_ELEMENT_ID {
outline: none;
}
""".trimIndent()
)
)
}
)
fun HTMLCanvasElement.fillViewportSize() {
setAttribute("width", "${window.innerWidth}")
setAttribute("height", "${window.innerHeight}")
}
var canvas = (document.getElementById(CANVAS_ELEMENT_ID) as HTMLCanvasElement).apply {
fillViewportSize()
}
ComposeWindow().apply {
window.addEventListener("resize", {
val newCanvas = canvas.cloneNode(false) as HTMLCanvasElement
canvas.replaceWith(newCanvas)
canvas = newCanvas
val scale = layer.layer.contentScale
newCanvas.fillViewportSize()
layer.layer.attachTo(newCanvas)
layer.layer.needRedraw()
layer.setSize((newCanvas.width / scale).toInt(), (newCanvas.height / scale).toInt())
})
// WORKAROUND: ComposeWindow does not implement `setTitle(title)`
val htmlTitleElement = (
htmlHeadElement.getElementsByTagName("title").item(0)
?: document.createElement("title").also { htmlHeadElement.appendChild(it) }
) as HTMLTitleElement
htmlTitleElement.textContent = title
setContent {
content(this)
}
}
}Oliver.O
08/09/2022, 10:19 PMimport androidx.compose.material.Text
import org.jetbrains.skiko.wasm.onWasmReady
fun main() {
onWasmReady {
BrowserViewportWindow("My Compose Application") {
Text("Hello Compose for Web/Canvas!")
}
}
}darkmoon_uk
08/09/2022, 11:05 PMhfhbd
08/10/2022, 3:45 AMGreg Steckman
08/10/2022, 3:54 AMGreg Steckman
08/10/2022, 3:56 AMOliver.O
08/10/2022, 2:08 PMindex.html. @Greg Steckman You should be able to safely remove styles.css and the attributes width="1024" height="768" from the HTML canvas element for clarity, as [bB]rowserViewportWindow would override those anyway.
Apart from that, consider the function an easy, slightly hacky, but viable entry point into Compose for Web on Canvas. I hope that as soon as stuff is easy to use, more people will try. The idea of having the entire Compose UI (non-DOM) infrastructure available on the Web is so enticing (productivity-wise, but also performance-wise). Of course, everything is still at an early stage.Greg Steckman
08/10/2022, 2:28 PMOliver.O
08/10/2022, 2:36 PMMichael Paus
08/11/2022, 12:11 PMMichael Paus
08/11/2022, 12:50 PMOliver.O
08/11/2022, 12:55 PMMichael Paus
08/11/2022, 1:51 PMwindow.addEventListener("resize", {
// val newCanvas = canvas.cloneNode(false) as HTMLCanvasElement
// canvas.replaceWith(newCanvas)
// canvas = newCanvas
val scale = layer.layer.contentScale
canvas.fillViewportSize()
layer.layer.attachTo(canvas)
layer.layer.needRedraw()
layer.setSize((canvas.width / scale).toInt(), (canvas.height / scale).toInt())
})
and my app works as before but I am wondering why you had the commented out part in there. Am I missing something?Oliver.O
08/11/2022, 1:59 PMagrosner
08/19/2022, 4:04 PMagrosner
08/19/2022, 4:04 PMthelumiereguy
10/19/2022, 6:51 PMArkadii Ivanov
02/14/2023, 10:30 AMOliver.O
02/14/2023, 10:36 AMArkadii Ivanov
02/14/2023, 10:41 AMMichael Paus
02/14/2023, 1:39 PMval density = LocalDensity.current
and then you can use
val width = with(density) { someWidthInPixels.toDp() }
to get the normal width in dp.
I also have a Mac and I don’t see the problem you describe when using the above code.Arkadii Ivanov
02/14/2023, 1:42 PMMichael Paus
02/14/2023, 1:47 PMArkadii Ivanov
02/14/2023, 2:59 PMMichael Paus
02/14/2023, 3:12 PMArkadii Ivanov
02/14/2023, 3:35 PMBrowserViewportWindow and index.html and my project still has the issue. However, I have just cloned and ran PolySpiralMpp and it works fine. I still can't find the cause.Arkadii Ivanov
02/14/2023, 3:44 PM1.3.0 and Kotlin from 1.8.0 to 1.3.0-alpha01-dev849 and 1.7.20 respectively solves the issue for me. Wondering whether this is a regression or the implementation of BrowserViewportWindow should be updated to match some internal changes?Michael Paus
02/14/2023, 3:46 PMArkadii Ivanov
02/14/2023, 3:47 PMdarkmoon_uk
02/20/2023, 11:00 AMlayer.layer.contentScale and setting scale to 1.0 fixes it on my MacBook too; which begs the question: Is layer.layer.contentScale even supposed to be related to screen density or was this perhaps a mistaken assumption?darkmoon_uk
02/20/2023, 11:08 AMdarkmoon_uk
02/20/2023, 11:13 AMOliver.O
02/20/2023, 11:51 AMdarkmoon_uk
02/20/2023, 12:20 PMdarkmoon_uk
02/20/2023, 12:30 PMdarkmoon_uk
02/20/2023, 12:30 PMArjan van Wieringen
02/24/2023, 6:22 PMComposeWindow().apply {
window.addEventListener("resize", {
val newCanvas = canvas.cloneNode(false) as HTMLCanvasElement
val density = window.devicePixelRatio.toFloat() // <-- get density
canvas.replaceWith(newCanvas)
canvas = newCanvas
val scale = layer.layer.contentScale
newCanvas.fillViewportSize()
layer.layer.attachTo(newCanvas)
layer.layer.needRedraw()
// and use it here
layer.setSize((newCanvas.width / scale * density).toInt(), (newCanvas.height / scale * density).toInt())
})Arjan van Wieringen
02/24/2023, 6:27 PMOliver.O
02/24/2023, 6:45 PMwindow.devicePixelRatio = 1, so that appears to be the differentiator. Thanks for sharing!Oliver.O
02/24/2023, 6:57 PMZeeshan Syed
02/28/2023, 12:57 PMOliver.O
02/28/2023, 1:28 PMZeeshan Syed
03/01/2023, 6:11 AMArjan van Wieringen
03/04/2023, 7:41 AMOliver.O
03/04/2023, 10:56 AMOliver.O
03/05/2023, 9:09 PMChris Sinco [G]
07/01/2023, 10:39 PMLol, scale = 2 and density = 2, so they cancel each other out.... don't know if they are always the same, but I'll keep this for now.Following up on this, I actually had to remove the scale/density calculation. I believe in web, the pixel dimensions returned from window in JavaScript are density independent already. So the resize listener is even simpler
window.addEventListener("resize", {
canvas.fillViewportSize()
layer.layer.attachTo(canvas)
layer.setSize(canvas.width, canvas.height)
layer.layer.needRedraw()
})Sunil Kumar
07/30/2023, 7:07 AMIrLinkageError: Property accessor 'layer.<get-layer>' can not be called: Private property accessor declared in module <org.jetbrains.compose.ui:ui> can not be accessed in module <compose-instagram-clone-multiplatform:webApp>\n
we are not able to access layer(ComposeLayer) from ComposeWindow now, Is there any workaround for thisOliver.O
07/30/2023, 9:02 AMSunil Kumar
07/31/2023, 7:20 AMOliver.O
07/31/2023, 8:25 AMBrowserViewportWindow and AFAIK starring it is the easiest way to get notified about updates.Sunil Kumar
08/03/2023, 7:41 AMOliver.O
11/01/2023, 9:32 PMBrowserViewportWindow is no longer needed: Compose Multiplatform releases since 1.5.0-beta02 contain CanvasBasedWindow, a fully featured replacement for all functionality formerly provided by BrowserViewportWindow. It works identically on js and jsWasm targets, and is an integral part of Compose Multiplatform. Say goodbye to BrowserViewportWindow, we had a fun time. Hello CanvasBasedWindow! 😃
compose-counting-grid has been updated accordingly, reflecting the latest changes.Chris Athanas
04/04/2024, 10:31 PM<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"> <!-- note: no meta viewport tag here! -->
<title>Compose App</title>
<script>
// Sets the viewport to the correct size for mobile devices
window.addEventListener("load", function(){ onLoad() } );
function onLoad() {
window.setTimeout(function() {
const meta = document.createElement('meta');
meta.name = "viewport";
meta.content = "width=" + (window.screen.width * window.devicePixelRatio) / 2 + ", initial-scale=1";
document.head.appendChild(meta);
}, 0);
}
</script>
Here’s the project: https://github.com/realityexpander/KMPMensAdventureThierry Kh
05/26/2025, 8:15 PMOliver.O
05/26/2025, 9:01 PMCanvasBasedWindow with its requestResize parameter set to a lambda that returns the size of your canvas element?Thierry Kh
05/26/2025, 9:03 PMOliver.O
05/26/2025, 9:07 PMThierry Kh
05/26/2025, 9:08 PM