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

Update web implementation to share Compose #15

Merged
merged 1 commit into from May 28, 2023
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 change: 1 addition & 0 deletions build.gradle.kts
Expand Up @@ -15,5 +15,6 @@ allprojects {
google()
mavenCentral()
mavenLocal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}
5 changes: 4 additions & 1 deletion gradle.properties
Expand Up @@ -14,4 +14,7 @@ android.useAndroidX=true
org.jetbrains.compose.experimental.uikit.enabled=true

# https://github.com/JetBrains/compose-jb/issues/2046
kotlin.native.cacheKind=none
kotlin.native.cacheKind=none

# enable jscanvas
org.jetbrains.compose.experimental.jscanvas.enabled=true
11 changes: 10 additions & 1 deletion renderer/build.gradle.kts
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension

plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
Expand All @@ -11,6 +13,10 @@ kotlin {
jvm()
android()

js(IR) {
browser()
}

ios()
iosSimulatorArm64()

Expand Down Expand Up @@ -66,4 +72,7 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
}

// Use a proper version of webpack, TODO remove after updating to Kotlin 1.9.
rootProject.the<NodeJsRootExtension>().versions.webpack.version = "5.76.2"
7 changes: 6 additions & 1 deletion shared/build.gradle.kts
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension

plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
Expand Down Expand Up @@ -149,4 +151,7 @@ configurations.named("podReleaseFrameworkMacosArm64").configure {
attributes {
attribute(customAttribute, "release")
}
}
}

// Use a proper version of webpack, TODO remove after updating to Kotlin 1.9.
rootProject.the<NodeJsRootExtension>().versions.webpack.version = "5.76.2"
9 changes: 8 additions & 1 deletion web-compose/build.gradle.kts
Expand Up @@ -5,16 +5,23 @@ plugins {

kotlin {
js(IR) {
moduleName = "web-compose"
browser()
binaries.executable()
}
sourceSets {
val jsMain by getting {
dependencies {
implementation(compose.web.core)
implementation(compose.ui)
implementation(compose.runtime)
implementation(compose.foundation)
implementation(project(":shared"))
implementation(project(":renderer"))
}
}
}
}

compose.experimental {
web.application {}
}
8 changes: 4 additions & 4 deletions web-compose/kotlin-js-store/yarn.lock
Expand Up @@ -2933,10 +2933,10 @@ webpack-sources@^3.2.3:
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==

webpack@5.74.0:
version "5.74.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
webpack@5.76.2:
version "5.76.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.2.tgz#6f80d1c1d1e3bf704db571b2504a0461fac80230"
integrity sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"
Expand Down
80 changes: 80 additions & 0 deletions web-compose/src/jsMain/kotlin/BrowserViewportWindow.kt
@@ -0,0 +1,80 @@
@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.
* Taken from: https://kotlinlang.slack.com/archives/C01F2HV7868/p1660083398571449
*/
fun BrowserViewportWindow(
Copy link
Owner Author

Choose a reason for hiding this comment

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

Using this wrapper from Oliver on Slack instead of CanvasBasedWindow because it plays nicer with web browsers and resizing of the window.

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)
}
}
}
60 changes: 7 additions & 53 deletions web-compose/src/jsMain/kotlin/Main.kt
@@ -1,58 +1,12 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import app.salah.calculator.SalahCalculator
import app.salah.model.SalahTimes
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.css.padding
import org.jetbrains.compose.web.css.px
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.renderComposable
import app.salah.data.PrayerTimesRepository
import app.salah.view.PrayerTimesWrapperProvider
import org.jetbrains.skiko.wasm.onWasmReady

fun main() {
val calculator = SalahCalculator()
renderComposable(rootElementId = "root") {
val inputText = remember { mutableStateOf("") }
val searchText = remember { mutableStateOf("") }
val prayerTimes = remember { mutableStateOf<SalahTimes?>(null) }

Div({ style { padding(25.px) } }) {
Input(type = InputType.Text, attrs = {
onInput { inputText.value = it.value }
onKeyUp {
if (it.key == "Enter") {
searchText.value = inputText.value
}
}
})

Button(attrs = { onClick { searchText.value = inputText.value } }) {
Text("Get Timings")
}

Div {
val timings = prayerTimes.value
if (timings != null) {
Text(timings.name)
PrayerTimesRow("Fajr", timings.fajr)
PrayerTimesRow("Dhuhr", timings.dhuhr)
PrayerTimesRow("Asr", timings.asr)
PrayerTimesRow("Maghrib", timings.maghrib)
PrayerTimesRow("Isha", timings.isha)
}
}
}

LaunchedEffect(searchText.value) {
val query = searchText.value
prayerTimes.value = if (query.isBlank()) {
null
} else {
calculator.prayerTimes(query)
}
val prayerTimesRepository = PrayerTimesRepository()
onWasmReady {
BrowserViewportWindow("PrayerTimes") {
Copy link
Owner Author

Choose a reason for hiding this comment

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

Instead of directly using CanvasBasedWindow directly, we use BrowserViewportWindow so that resizing properly works in the browser.

PrayerTimesWrapperProvider.providePrayerTimesWrapper(prayerTimesRepository).invoke()
}
}
}
22 changes: 0 additions & 22 deletions web-compose/src/jsMain/kotlin/PrayerTimesRow.kt

This file was deleted.

5 changes: 3 additions & 2 deletions web-compose/src/jsMain/resources/index.html
Expand Up @@ -3,9 +3,10 @@
<head>
<meta charset="UTF-8">
<title>PrayerTimes Multiplatform</title>
<script src="skiko.js"> </script>
</head>
<body>
<div id="root"></div>
<script src="web-compose.js"></script>
<canvas id="ComposeTarget"></canvas>
<script src="web-compose.js"> </script>
</body>
</html>